ai-ax 0.4.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,146 +2,158 @@
2
2
 
3
3
  AI Agent Experience (AI/AX) library for Hono + Cloudflare.
4
4
 
5
- ai-ax lets you build the human experience (UI/UX) and the AI agent experience (AI/AX) of a web app as one surface. Every operation is declared exactly once, and that single declaration becomes a click handler for humans and an MCP tool for LLMs at the same time, backed by one realtime state that both sides read and write.
5
+ ai-ax builds the human experience (UI/UX) and the AI agent experience (AI/AX) of a web app as one surface. An operation is declared exactly once, and that one declaration is a click handler for the person watching the screen and an MCP tool for the LLM at the same time, backed by one realtime state that both of them read and write.
6
6
 
7
- Three ideas carry the whole library: **one action**, **one tree**, **one resolver**.
7
+ Three ideas carry the library — **one action**, **one tree**, **one road** — and each of them is a mechanism you can point at, not a slogan: one action is a single tool registry that serves the UI, the MCP server and the cross-side transport; one tree is a single yjs map that is rendered as the screen, dumped as the agent's observation and encoded as the persistence snapshot; one road is a single `ai` object that every runtime adapter receives.
8
8
 
9
9
  ## Why
10
10
 
11
- The web has been built on the assumption that a human is the one operating it. When an LLM path is bolted onto such an app afterwards, everything doubles: two operation surfaces, two auth stacks, two sets of bugs. The two copies inevitably drift apart, and the drift is usually discovered weeks later in a log.
11
+ The AI agent has become one more user of the web site. The web, however, was built on the assumption that a human is the one operating it, so when an agent path is bolted onto an app as a second route beside the browser, everything doubles: two operation surfaces, two auth stacks, two sets of bugs, roughly twice the code to manage. The two copies inevitably drift apart, and the drift is usually discovered weeks later in a log.
12
12
 
13
- ai-ax removes the second copy instead of maintaining it. The AI agent is treated as one more user of the app: it reads the same state and calls the same operations as the person watching the screen. When a developer debugs a button by clicking it in the UI, that debugged code is literally what the agent runs through MCP. There is no second implementation left to drift.
13
+ ai-ax removes the second copy instead of maintaining it. The person and the agent read the same state and call the same operations, so the declaration of an operation _is_ its MCP tool, and the implementation lives in exactly one place. Every operation is synchronized in realtime through partyserver, whether it arrived from a browser click or from an MCP request, so the agent's moves appear on the human's screen as they happen.
14
14
 
15
- ![one action, one tree](https://r.tsei.jp/ai-ax/concept.svg)
16
-
17
- **One action** — the smallest unit of operation is declared once and reached from both the UI and MCP. **One tree** — all state lives in a single yjs tree, which humans see as the rendered screen and the LLM observes as `get_tree`. **One resolver** — authorization is a single function that decides which room a user may enter; every path, human or AI, goes through it.
15
+ Authentication is the one place where the two users genuinely differ, because an MCP request from the Claude API cannot carry the browser's session. ai-ax closes that gap with a signed token: when the user sends a prompt, the Hono side mints a token and hands it to the Claude API, the token comes back on every MCP request, and the MCP server verifies the signature before executing anything.
18
16
 
19
17
  ## What
20
18
 
21
- The assumed stack is Hono + Cloudflare Workers. Realtime sync is partyserver (Durable Objects) with yjs, authentication is Auth.js, and the LLM is the Claude API with its MCP connector. ai-ax ships the server-side plumbing between them — authentication, authorization, realtime sync, the MCP server, and the LLM relay — as Hono middleware.
22
-
23
- ![stack](https://r.tsei.jp/ai-ax/stack.svg)
24
-
25
- The worker needs one composite middleware, and the Durable Object is a re-export.
19
+ The assumed stack is Hono on Cloudflare Workers. Realtime sync is partyserver — the successor of partykit, which was deprecated after becoming a Cloudflare product — running yjs docs on Durable Objects deployed straight to Cloudflare. Authentication is Auth.js, and the LLM is the Claude API with its MCP connector. Hono itself is confined to the `ai-ax/hono` entry, so every other entry stays framework free.
26
20
 
27
- ```ts
28
- import { Hono } from 'hono'
29
- import { createAiax, ownSelf } from 'ai-ax'
30
- import { PROMPT } from './prompt'
31
- import './actions'
21
+ The whole library is one loop: the browser and the agent join the same room, and every operation — clicked or tool-called — lands on the same doc and syncs back to both.
32
22
 
33
- const ai = createAiax({ name: 'demo', own: ownSelf(), database: (e) => e.my_d1, system: PROMPT })
34
- const app = new Hono().use('*', ai.all())
35
-
36
- export default app
37
- export { AiaxServer } from 'ai-ax/server'
38
- ```
39
-
40
- `ai.all()` routes `/parties/*` to websocket sync, `/api/mcp` to the MCP server, `POST /api/llm` to the Claude relay, and handles Auth.js sessions; everything else falls through to your own routes. The exported `AiaxServer` class is deployed as a Durable Object under the binding name `v1`.
23
+ ![diagram](https://r.tsei.jp/ai-ax/diagram.svg)
41
24
 
42
25
  ### One action
43
26
 
27
+ `x(params, config)` builds the `ai` object. The first argument declares typed state keys with their initial values (write `x<Keys>()` when a key should start undefined), and the second argument is the service config.
28
+
29
+ <!-- prettier-ignore -->
44
30
  ```ts
45
- import { action } from 'ai-ax/action'
31
+ // actions.ts
32
+ import { x } from 'ai-ax'
46
33
  import { z } from 'zod'
47
34
 
48
- export const placeMark = action<{ cell: number }, string>(() => import('./actions/place-mark'), 'both', {
49
- name: 'place_mark',
50
- description: 'Place the current player mark on an empty cell',
51
- input: { cell: z.number().describe('cell index 0-8') },
52
- })
35
+ export default x(
36
+ { count: 0 },
37
+ { name: 'counter', tree: 'count is the current value. Observe before acting', own: ({ sub }) => sub },
38
+ )
39
+ .describe('A counting app.', (a) =>
40
+ a
41
+ .action('increment', 'Add 1 to the current value', () => ++ai.count)
42
+ .action('overwrite', 'Overwrite the current value', { count: z.number() }, ({ count }) => (ai.count = count)),
43
+ )
44
+ .client('notify', 'Show an alert in the browser', { text: z.string() }, ({ text }) => alert(text))
45
+ .server('heavy', 'Run heavy work on the server', { src: z.string() }, () => import('./heavy'))
53
46
  ```
54
47
 
55
- This one declaration yields two things: an async function the UI calls from event handlers, and an MCP tool (name, description, zod input schema) exposed to the LLM. The first argument is either a dynamic import or the implementation function itself, so bundles can stay split or stay simple.
48
+ Each definition takes the tool name first, and after that the description, the zod schema and the implementation in any order — each part is recognized by its type, so an inline handler reads hono-like as `(name, schema, fn)` while a dynamic import reads `(name, () => import('./x'), description, schema)` with the schema at the end where the whole list scans best. The implementation is either an inline function or a dynamic import whose default export receives `(input, ai)`, so heavy or browser-incompatible code stays out of the other side's bundle. `.action` runs on whichever side called it, `.client` only in the browser, `.server` only in the Durable Object; when the other side calls it, the request travels over the shared doc and the result travels back, so a tool call from the LLM can open a dialog in the connected browser and return what the user did. `.describe(text, fn)` blocks nest, and their texts concatenate hierarchically into the MCP tool description. `get_tree` is built in — every service exposes the same observation tool, and `config.tree` replaces its description.
56
49
 
57
- `side` states where the implementation is able to run, and ai-ax forwards calls across the boundary when the other side needs it.
50
+ The declared names and schemas flow into the type of `ai`: `ai.overwrite({ count: 3 })` typechecks, `ai.overwrite(true)` and `ai.missing()` do not.
58
51
 
59
- | side | runs on | when called from the other side |
60
- | -------- | -------------------------------- | ---------------------------------------------------------- |
61
- | `both` | browser and Durable Object | executed locally on whichever side called it |
62
- | `client` | browser only (needs browser API) | delegated to the connected browser via yjs act-req/act-res |
63
- | `server` | Durable Object only | forwarded to the Durable Object |
52
+ ### One tree
64
53
 
65
- Inside an action, `data()` returns the shared `Y.Map` named `tree`. Whatever is written there syncs to every connected browser in realtime and is exactly what the LLM observes, so the agent's moves appear on the human's screen as they happen.
54
+ All shared state lives in a single yjs map. State declared in the first argument is read and written through plain properties — `ai.count = 3` above syncs to every connected browser and to the agent's next `get_tree` — and any undeclared key travels the same proxy: `ai['files/a.txt'] = entry`, `ai['files/a.txt']` and `delete ai['files/a.txt']` are how repeated rows live under a `name/id` convention. `ai.rows(prefix)` lists the direct children of a prefix sorted by `pos`, `ai.scan(prefix)` lists every descendant, `ai.wipe(prefix)` clears them, `ai.tx(fn, origin?)` groups writes into one transaction, and `ai.history(options?)` is the undo manager over the same map. `ai.data()` exposes the underlying `Y.Map` only for what a property can never express — `observe` subscriptions and whole-tree iteration.
66
55
 
67
- The browser side connects once and everything above starts working.
56
+ The tree is also the persistence unit. `config.load` restores a snapshot when the doc first wakes, `config.save` persists one on the update debounce, on a durable alarm and on every disconnect — which is what survives Durable Object hibernation — and `ai.snapshot()` encodes it. `config.ui` names the volatile keys (a trailing `/` marks a prefix): they still sync and still show up in `get_tree`, but they are excluded from both the snapshot and the undo capture, so a saved file holds the work and not the open menus. `config.history` merges extra `Y.UndoManager` options such as `captureTimeout`. A `lobby` room skips persistence entirely, so an anonymous top page stays playable without touching storage. State that should stay out of the shared tree entirely deliberately steps off the road: the `ai-ax/react` entry ships a small atom store (`atom`, `setAtom`, `useAtomValue`) for browser-local values like an IME buffer or a chat stream.
68
57
 
69
58
  ```ts
70
- import { ALIAS, connect } from 'ai-ax/client'
71
-
72
- const link = connect({ room: ALIAS })
73
- link.synced(() => boot())
59
+ x(
60
+ {},
61
+ {
62
+ name: 'notes',
63
+ lobby: 'notes',
64
+ own: async ({ env, sub, path }) => (await env.my_bucket.head(`${sub}/${path}`))?.customMetadata?.site ?? '',
65
+ load: async ({ env, sub, path }) => (await env.my_bucket.get(`${sub}/${path}`))?.arrayBuffer(),
66
+ save: ({ env, sub, path, room }, buf) => env.my_bucket.put(`${sub}/${path}`, buf, { customMetadata: { site: room } }),
67
+ },
68
+ )
74
69
  ```
75
70
 
76
- Clients always connect to the alias room `my-room`. The server rewrites it to the authorized room, so no client ever picks a real room name.
71
+ ### One road
77
72
 
78
- ### One resolver
79
-
80
- Only two kinds of credentials exist, and both are jwt signed with `AUTH_SECRET`; a plain user id never crosses the network.
81
-
82
- | path | credential | issued by | verified by |
83
- | -------------------------- | ---------------------------- | ------------------------------------------ | --------------------------------------- |
84
- | Browser → Worker | session cookie (Auth.js jwt) | `/api/auth` (Google OAuth) | `userSub()` |
85
- | Claude → Worker `/api/mcp` | grant jwt `{ sub, path }` | llm middleware, minted after authorization | `readGrant()` |
86
- | Worker → Durable Object | room name + `x-user-sub` | worker overwrites with authorized values | DO is reachable only via worker binding |
87
-
88
- Authorization concentrates into a single function.
73
+ The app touches the library through the one `ai` object, and each runtime has exactly one adapter that receives it.
89
74
 
75
+ <!-- prettier-ignore -->
90
76
  ```ts
91
- type Own = (env: any, sub: string, path: string) => Promise<string> | string
77
+ // index.ts (worker + Durable Object)
78
+ import { Hono } from 'hono'
79
+ import { hono } from 'ai-ax/hono'
80
+ import { Server } from 'ai-ax/server'
81
+ import { PROMPT } from './prompt'
82
+ import ai from './actions'
83
+
84
+ export default new Hono().use('*', hono(ai, { system: PROMPT }))
85
+ export class PartyServer extends Server {
86
+ ai = ai
87
+ }
92
88
  ```
93
89
 
94
- It answers one question — which room does this user get for this path — and an empty string denies. `ownSelf()` returns the user's own id (personal apps: everyone gets exactly one private room). `ownSite(bucket)` reads the file's metadata from R2 (editor apps: owning the file opens its room). A resolver that joins a D1 membership table fits the same type.
90
+ ```tsx
91
+ // client.tsx (browser)
92
+ import { useClient } from 'ai-ax/react'
93
+ import ai from './actions'
95
94
 
96
- The grant jwt is minted per turn by the llm middleware and carried by the Claude API to `/api/mcp` on every MCP request. The LLM cannot choose the token's contents, and rewriting it breaks the signature, so at any moment the agent is sealed inside the one room that was just authorized.
95
+ const App = () => {
96
+ const { count } = useClient(ai)
97
+ return <button onClick={() => ai.increment()}>{count}</button>
98
+ }
99
+ ```
97
100
 
98
- ![grant sequence](https://r.tsei.jp/ai-ax/grant.svg)
101
+ `hono(ai, extra?)` routes `/parties/*` to websocket sync, the MCP endpoint, the LLM relay and the Auth.js session, and lets everything else fall through to the app's own routes; `extra` merges server-only config such as the system prompt or an Auth.js `adapter`, so they never enter the browser bundle. `Server` is the Durable Object: the app subclass assigns it the one `ai` (`class PartyServer extends Server { ai = ai }`), and it records the owning user across hibernation, serializes action execution, and drives the `load` / `save` hooks. `useClient(ai)` connects once and re-renders on every tree change (`Client` is the component form, `client(ai)` the non-React form; `route.synced` fires after the first sync, and `route.room: ''` opens the local doc without any connection), and `hydrate(ai, entries)` fills that local doc from a published JSON snapshot without waking a Durable Object at all.
99
102
 
100
- ### LLM relay
103
+ Runtime-only values — Cloudflare bindings, the authorized user, the room — travel the same road instead of module globals. `.abstract(name)` declares a function that actions may call before anyone has implemented it, and `.override(name, (input, next, ai) => ...)` supplies or wraps implementations onion-style, where `next(input)` runs whatever was underneath. The Durable Object overrides one well-known abstract before every execution: `ai.peer()` returns the authorized `{ env, room, sub, path }`, and everything else an app needs (an R2 client, a sandbox runner) is an abstract the app declares in its actions and overrides in its `PartyServer`. Overrides intercept tools too, which is how a browser rewrites the variables the Claude API sends before an action runs.
101
104
 
102
- `POST /api/llm` with `{ path, prompt, model?, messages? }` authorizes the caller, mints a grant, calls the Claude API with the MCP server attached, and relays the response as a text stream. `messages` is prior conversation to prepend before the prompt. The library never stores history — what to persist, where, and in which shape is the application's decision, and whatever is passed here is simply replayed.
105
+ `createStream()` from `ai-ax/client` is the browser side of the relay's wire format: `POST` to the relay streams plain text in which a `[tool]` line marks each MCP call and a trailing `[usage]` line closes the turn, and `parse` / `send` / prior-`messages` replay are the pieces an app needs to render and continue conversations without touching the format itself.
103
106
 
104
- The stream is plain text: a `[tool] server name` line marks a tool call and a trailing `[usage] ...` line closes the turn. `createStream()` returns the browser-side counterpart of this wire format.
107
+ ### One resolver
105
108
 
106
- ```ts
107
- import { createStream } from 'ai-ax/client'
109
+ Authorization concentrates into the single `own` function: given `{ env, sub, path }` it answers which room this user may enter, and an empty string denies. Only two kinds of credentials exist, both jwt signed with `AUTH_SECRET` — a plain user id never crosses the network.
108
110
 
109
- const stream = createStream()
110
- const items = await stream.send({ prompt, path, model, messages }, (u) => draw(u.items))
111
- ```
111
+ | path | credential | issued by | verified by |
112
+ | ----------------------- | ---------------------------- | -------------------------------------------- | ------------------------------------------- |
113
+ | Browser → Worker | session cookie (Auth.js jwt) | `/api/auth` (Google OAuth by default) | the Auth.js session |
114
+ | Claude → Worker MCP | grant jwt `{ sub, path }` | the LLM relay, minted per prompt | signature check before every tool execution |
115
+ | Worker → Durable Object | room name + user headers | the worker overwrites with authorized values | the DO is reachable only through the worker |
112
116
 
113
- `parse` splits raw text into items of `{ text, tools }` with usage hidden, `send` posts and streams with parsed updates, and the symmetric pair `pack` / `unpack` converts a finished reply into a storable `{ reply, tools }` form and back — the pieces an app needs to render, persist, and replay conversations without ever touching the wire format itself.
117
+ Browsers always connect to the alias room `my-room`, and the worker rewrites it to the room `own` returned, so no client ever picks a real room name. The grant jwt is minted when the user sends a prompt and is carried by the Claude API back on every MCP request; the LLM cannot choose the token's contents, and rewriting it breaks the signature, so at any moment the agent is sealed inside the one room that was just authorized, for at most `maxAge` seconds.
114
118
 
115
119
  ## How
116
120
 
117
- | entry | main exports | where it runs |
118
- | -------------- | ---------------------------------------- | ------------------------- |
119
- | `ai-ax` | `createAiax`, `ownSelf`, `ownSite` | worker (Hono middleware) |
120
- | `ai-ax/client` | `connect`, `createStream`, `ALIAS` | browser |
121
- | `ai-ax/action` | `action`, `actions`, `data`, `side` | shared action definitions |
122
- | `ai-ax/server` | `AiaxServer` | Durable Object |
123
- | `ai-ax/const` | protocol constants (paths, header names) | both |
124
-
125
- `createAiax(config)` accepts the following and returns `{ all, auth, party, mcp, llm }` — `all()` is the composite shown above, and the four factories are also usable individually.
126
-
127
- | key | type | default | meaning |
128
- | --------- | --------------------- | ----------------------------------------- | ------------------------------------------------------------ |
129
- | name | `string` | required | service id, becomes the MCP server name |
130
- | own | `Own` | required | authorization resolver, `''` denies |
131
- | database | `(env) => D1Database` | none | D1 for the Auth.js DrizzleAdapter, jwt-only when omitted |
132
- | lobby | `string` | none | one anonymous demo room, all paths require auth when omitted |
133
- | alias | `string` | `'my-room'` | connection name meaning "my room", rewritten by `own` |
134
- | system | `string` | `''` | system prompt for the LLM |
135
- | model | `string` | `'claude-sonnet-5'` | Claude API model id |
136
- | claude | `string` | `'https://api.anthropic.com/v1/messages'` | Claude API URL, replaceable with an AI Gateway |
137
- | maxTokens | `number` | `32000` | max_tokens per response |
138
- | maxTurns | `number` | `10` | upper bound of pause_turn continuations |
139
- | binding | `string` | `'v1'` | Durable Object binding name |
140
- | endpoint | `string` | `'/api/mcp'` | MCP endpoint path |
141
- | version | `string` | `'1.0.0'` | MCP server version string |
142
- | salt | `string` | `'authjs.mcp-token'` | grant jwt salt, separates services |
143
- | maxAge | `number` | `43200` (12h) | grant jwt lifetime in seconds |
144
- | cookie | `{ apex, services }` | none | services sharing a session cookie across subdomains |
145
- | providers | `any[]` | Google | Auth.js providers |
121
+ | entry | main exports | where it runs |
122
+ | -------------- | -------------------------------------------------------- | ----------------------- |
123
+ | `ai-ax` | `x` | everywhere (isomorphic) |
124
+ | `ai-ax/hono` | `hono` | worker |
125
+ | `ai-ax/server` | `Server` | Durable Object |
126
+ | `ai-ax/client` | `client`, `hydrate`, `createStream` | browser |
127
+ | `ai-ax/react` | `useClient`, `Client`, `atom`, `setAtom`, `useAtomValue` | browser (React) |
128
+ | `ai-ax/const` | protocol constants (paths, header names) | both |
129
+
130
+ The worker expects a Durable Object binding named `v1` pointing at the exported `Server` class, and the environment needs `AUTH_SECRET`, the OAuth client credentials and `ANTHROPIC_API_KEY`. Config keys of `x(params, config)` and `hono(ai, extra)`:
131
+
132
+ | key | type | default | meaning |
133
+ | --------- | -------------------------- | ----------------------------------------- | ----------------------------------------------------- |
134
+ | name | `string` | `'x'` | service id, becomes the MCP server name |
135
+ | tree | `string` | generic text | description of the built-in `get_tree` tool |
136
+ | own | `(at: Peer) => string` | deny all | authorization resolver, `''` denies |
137
+ | adapter | `(env) => Adapter` | none | Auth.js adapter factory, jwt-only when omitted |
138
+ | lobby | `string` | none | one anonymous room without persistence |
139
+ | load | `(at: Saved) => buf` | none | snapshot restore on first doc load |
140
+ | save | `(at: Saved, buf) => void` | none | snapshot persist on debounce / alarm / disconnect |
141
+ | ui | `string[]` | `[]` | volatile keys (trailing `/` = prefix), no undo / save |
142
+ | history | `object` | none | extra `Y.UndoManager` options (e.g. `captureTimeout`) |
143
+ | alias | `string` | `'my-room'` | connection name meaning "my room", rewritten by `own` |
144
+ | system | `string` | `''` | system prompt for the LLM |
145
+ | model | `string` | `'claude-sonnet-5'` | Claude API model id |
146
+ | claude | `string` | `'https://api.anthropic.com/v1/messages'` | Claude API URL, replaceable with an AI Gateway |
147
+ | maxTokens | `number` | `32000` | max_tokens per response |
148
+ | maxTurns | `number` | `10` | upper bound of pause_turn continuations |
149
+ | binding | `string` | `'v1'` | Durable Object binding name |
150
+ | host | `string` | request host | public host the Claude API reaches the MCP through |
151
+ | endpoint | `string` | `'/api/mcp'` | MCP endpoint path |
152
+ | relay | `string` | `'/api/llm'` | LLM relay path |
153
+ | version | `string` | `'1.0.0'` | MCP server version string |
154
+ | salt | `string` | `'authjs.mcp-token'` | grant jwt salt, separates services |
155
+ | maxAge | `number` | `43200` (12h) | grant jwt lifetime in seconds |
156
+ | cookie | `{ apex, services }` | none | services sharing a session cookie across subdomains |
157
+ | providers | `any[]` | Google | Auth.js providers |
146
158
 
147
159
  The `x-user-sub` / `x-user-path` header names and the anthropic-version are internal protocol constants and intentionally not configurable, because both ends of each protocol must change together.
package/dist/client.d.ts CHANGED
@@ -1,49 +1,2 @@
1
- import { ActMeta, ActionDef, Side } from "./types.js";
2
- import { Call, action, actions, bind, data, init, side } from "./action.js";
3
- import { ALIAS, BINDING } from "./const.js";
4
- import * as Y from "yjs";
5
- //#region src/browser/stream.d.ts
6
- interface StreamTool {
7
- server: string;
8
- name: string;
9
- }
10
- interface StreamItem {
11
- text: string;
12
- tools: StreamTool[];
13
- error?: boolean;
14
- }
15
- interface StreamSend {
16
- prompt: string;
17
- path?: string;
18
- model?: string;
19
- messages?: any[];
20
- headers?: Record<string, string>;
21
- }
22
- interface StreamUpdate {
23
- items: StreamItem[];
24
- delta: string;
25
- raw: string;
26
- }
27
- declare const createStream: (endpoint?: string) => {
28
- parse: (raw: string) => StreamItem[];
29
- send: (input: StreamSend, update?: (u: StreamUpdate) => void) => Promise<StreamItem[]>;
30
- };
31
- //#endregion
32
- //#region src/client.d.ts
33
- interface Route {
34
- room?: string;
35
- path?: string;
36
- host?: string;
37
- party?: string;
38
- doc?: Y.Doc;
39
- }
40
- interface Link {
41
- doc: Y.Doc;
42
- call: Call;
43
- synced: (fn: () => void) => void;
44
- leave: () => void;
45
- }
46
- declare const onLeave: (fn: () => void) => undefined;
47
- declare const connect: (route?: Route) => Link;
48
- //#endregion
49
- export { ALIAS, type ActMeta, type ActionDef, BINDING, Link, Route, type Side, type StreamItem, type StreamSend, type StreamTool, type StreamUpdate, action, actions, bind, connect, createStream, data, init, onLeave, side };
1
+ import { a as StreamSend, c as createStream, i as StreamItem, n as client, o as StreamTool, r as hydrate, s as StreamUpdate, t as Route } from "./client-DG7hCEbP.js";
2
+ export { Route, type StreamItem, type StreamSend, type StreamTool, type StreamUpdate, client, createStream, hydrate };
package/dist/client.js CHANGED
@@ -1,2 +1,2 @@
1
- import{ALIAS as e,BINDING as t,LLM_PATH as n}from"./const.js";import{action as r,actions as i,bind as a,data as o,init as s,side as c}from"./action.js";import l from"y-partyserver/provider";import*as u from"yjs";const d=/^\[tool\] (?:(\S+) )?(\S+)$/,f=(e=n)=>{let t=e=>{let t=[{text:``,tools:[]}],n=!1;for(let r of e.split(`
2
- `)){if(r.startsWith(`[usage]`))continue;let e=d.exec(r),i=t[t.length-1];if(e){i.tools.push({server:e[1]??``,name:e[2]}),n=!0;continue}if(!(n&&r.trim()===``)){if(n){t.push({text:r,tools:[]}),n=!1;continue}i.text=i.text===``?r:`${i.text}\n${r}`}}return t.map(e=>({...e,text:e.text.trim()})).filter(e=>e.text!==``||e.tools.length>0)};return{parse:t,send:async(n,r)=>{let{headers:i={},...a}=n,o=await fetch(e,{method:`POST`,headers:{"content-type":`application/json`,...i},body:JSON.stringify(a)});if(!o.ok){let e=(await o.text()).replace(/\n/g,` `),t=[{text:e,tools:[],error:!0}];return r?.({items:t,delta:e,raw:e}),t}let s=o.body.getReader(),c=new TextDecoder,l=``;for(;;){let{done:e,value:n}=await s.read();if(e)break;let i=c.decode(n,{stream:!0});l+=i,r?.({items:t(l),delta:i,raw:l})}return t(l)}}};let p=null,m=new Set;const h=e=>void m.add(e),g=(e={})=>{p?.leave();let{room:t=``,path:n=``,host:r=location.host,party:i=`v1`,doc:a}=e,o=t?new l(r,t,a,{party:i,params:{path:n}}):null,c=o?.doc??a??new u.Doc;return p={doc:c,call:s(c),synced(e){if(!o||o.synced)return void e();o.once(`synced`,()=>e())},leave(){m.forEach(e=>e()),m=new Set,o?.destroy()}}};export{e as ALIAS,t as BINDING,r as action,i as actions,a as bind,g as connect,f as createStream,o as data,s as init,h as onLeave,c as side};
1
+ import{ALIAS as e,LLM_PATH as t}from"./const.js";import n from"y-partyserver/provider";const r=/^\[tool\] (?:(\S+) )?(\S+)$/,i=(e=t)=>{let n=e=>{let t=[{text:``,tools:[]}],n=!1;for(let i of e.split(`
2
+ `)){if(i.startsWith(`[usage]`))continue;let e=r.exec(i),a=t[t.length-1];if(e){a.tools.push({server:e[1]??``,name:e[2]}),n=!0;continue}if(!(n&&i.trim()===``)){if(n){t.push({text:i,tools:[]}),n=!1;continue}a.text=a.text===``?i:`${a.text}\n${i}`}}return t.map(e=>({...e,text:e.text.trim()})).filter(e=>e.text!==``||e.tools.length>0)};return{parse:n,send:async(t,r)=>{let{headers:i={},...a}=t,o=await fetch(e,{method:`POST`,headers:{"content-type":`application/json`,...i},body:JSON.stringify(a)});if(!o.ok){let e=(await o.text()).replace(/\n/g,` `),t=[{text:e,tools:[],error:!0}];return r?.({items:t,delta:e,raw:e}),t}let s=o.body.getReader(),c=new TextDecoder,l=``;for(;;){let{done:e,value:t}=await s.read();if(e)break;let i=c.decode(t,{stream:!0});l+=i,r?.({items:n(l),delta:i,raw:l})}return n(l)}}},a=new WeakSet,o=(t,r={})=>{if(a.has(t))return t;a.add(t);let{room:i=e,path:o=``,host:s=location.host,party:c=`v1`,synced:l}=r,u=t.open().data().doc,d=i?new n(s,i,u,{party:c,params:{path:o}}):null;return l&&(!d||d.synced?l():d.once(`synced`,()=>l())),t},s=(e,t,n=(e,t)=>t)=>{if(a.has(e))return e;a.add(e);let r=e;return r.open().tx(()=>Object.entries(t).forEach(([e,t])=>r[e]=n(e,t))),e};export{o as client,i as createStream,s as hydrate};
package/dist/hono.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { Ai, Config } from "./types.js";
2
+ //#region src/hono.d.ts
3
+ declare const hono: (ai: Ai<any>, extra?: Partial<Config>) => import("hono").MiddlewareHandler<any, string, {}, Response>;
4
+ //#endregion
5
+ export { hono };
package/dist/hono.js ADDED
@@ -0,0 +1,2 @@
1
+ import{PATH_HEADER as e,SUB_HEADER as t}from"./const.js";import{createMiddleware as n}from"hono/factory";import r from"@auth/core/providers/google";import{decode as i,encode as a}from"@auth/core/jwt";import{getAuthUser as o,initAuthConfig as s}from"@hono/auth-js";import{env as c}from"hono/adapter";import{streamText as l}from"hono/streaming";import{StreamableHTTPTransport as u}from"@hono/mcp";import{McpServer as d}from"@modelcontextprotocol/sdk/server/mcp.js";import{getServerByName as f,routePartykitRequest as p}from"partyserver";const m=`authjs.mcp-token`,h=e=>s(t=>{let n=new URL(t.req.url).host,i=!!e.cookie&&e.cookie.services.some(t=>n===`${t}.${e.cookie.apex}`),a={sessionToken:{options:{domain:`.${e.cookie?.apex}`}}},o=e.providers??[r({clientId:t.env.GOOGLE_CLIENT_ID,clientSecret:t.env.GOOGLE_CLIENT_SECRET})];return{...e.adapter?{adapter:e.adapter(t.env)}:{},providers:o,secret:t.env.AUTH_SECRET,session:{strategy:`jwt`},...i?{cookies:a}:{}}}),g=async e=>(await o(e))?.token?.sub??``,_=(e,t,n,r)=>a({token:{sub:t,path:n},secret:c(e).AUTH_SECRET,salt:r?.salt??m,maxAge:r?.maxAge??43200}),v=async(e,t)=>{let n=(e.req.header(`Authorization`)??``).replace(/^Bearer\s+/i,``);if(!n)return null;let r=await i({token:n,secret:c(e).AUTH_SECRET,salt:t?.salt??m}).catch(()=>null);return{sub:r?.sub??``,path:r?.path??``}},y=e=>e.startsWith(`data:`)?JSON.parse(e.slice(5)):null,b=(e,t,n)=>fetch(e.api,{method:`POST`,headers:{"x-api-key":e.key,"anthropic-version":`2023-06-01`,"anthropic-beta":`mcp-client-2025-11-20`,"content-type":`application/json`},body:JSON.stringify({model:e.model,max_tokens:e.maxTokens,stream:!0,cache_control:{type:`ephemeral`},system:e.system,mcp_servers:[{type:`url`,url:e.url,name:e.name,...t?{authorization_token:t}:{}}],tools:[{type:`mcp_toolset`,mcp_server_name:e.name}],messages:n})}),x=async(e,t)=>{let n=[],r=null,i=0,a=0,o=0,s=``,c=e.body.getReader(),l=new TextDecoder,u=``;for(;;){let{done:e,value:d}=await c.read();if(e)break;u+=l.decode(d,{stream:!0});let f=u.split(`
2
+ `);u=f.pop()??``;for(let e of f){let c=y(e);if(c){if(c.type===`message_start`){let e=c.message?.usage??{};i+=(e.input_tokens??0)+(e.cache_creation_input_tokens??0)+(e.cache_read_input_tokens??0),a+=e.cache_read_input_tokens??0}c.type===`message_delta`&&(o=c.usage?.output_tokens??o,s=c.delta?.stop_reason??s),c.type===`content_block_start`&&(r={...c.content_block},n.push(r),r.type===`mcp_tool_use`&&await t(`\n[tool] ${r.server_name} ${r.name}\n`)),c.type===`content_block_delta`&&c.delta?.type===`text_delta`&&(r.text=(r.text??``)+c.delta.text,await t(c.delta.text)),c.type===`content_block_delta`&&c.delta?.type===`input_json_delta`&&(r.partial=(r.partial??``)+c.delta.partial_json),c.type===`content_block_delta`&&c.delta?.type===`thinking_delta`&&(r.thinking=(r.thinking??``)+c.delta.thinking),c.type===`content_block_delta`&&c.delta?.type===`signature_delta`&&(r.signature=(r.signature??``)+c.delta.signature),c.type===`content_block_stop`&&r?.partial!==void 0&&(r.input=r.partial?JSON.parse(r.partial):{},delete r.partial)}}}return{blocks:n,input:i,cached:a,output:o,stop:s}},S=async(e,t,n,r,i,a=10)=>{let o=r,s=0,c=0,l=0,u=``;for(let r=0;r<a;r++){let r=await x(o,i);if(s+=r.input,c+=r.cached,l+=r.output,u=r.stop||u,u!==`pause_turn`)break;if(n.push({role:`assistant`,content:r.blocks.filter(e=>e.type!==`thinking`||e.thinking||e.signature)}),o=await b(e,await t(),n),!o.ok){await i(`\n[error] ${(await o.text()).replace(/\n/g,` `)}\n`);break}}await i(`\n[usage] input ${s} (cache ${c}) / output ${l} tokens (${u})\n`)},C=e=>n(async t=>{let n=await g(t),{path:r=``,prompt:i,model:a=``,messages:o=[]}=await t.req.json(),s=!!e.lobby&&!r;if(!s&&!n)return t.text(`Unauthorized`,401);if(!s&&!await(e.own??(()=>``))({env:c(t),sub:n,path:r}))return t.text(`Forbidden`,403);let u=e.host??t.req.header(`x-original-host`)??t.req.header(`x-forwarded-host`)??new URL(t.req.url).host,d={key:c(t).ANTHROPIC_API_KEY,api:e.claude??`https://api.anthropic.com/v1/messages`,name:e.name??`aiax`,url:`https://${u}${e.endpoint??`/api/mcp`}`,model:a||e.model||`claude-sonnet-5`,system:e.system??``,maxTokens:e.maxTokens??32e3},f=()=>s?Promise.resolve(``):_(t,n,r,e),p=[...o,{role:`user`,content:i}],m=await b(d,await f(),p);return m.ok?l(t,t=>S(d,f,p,m,e=>t.write(e),e.maxTurns??10)):t.text(await m.text(),500)}),w=async(n,r,i,a,o)=>{let s=await f(n,r),c={[t]:i.sub,[e]:encodeURIComponent(i.path)};return(await s.fetch(`https://internal/act`,{method:`POST`,headers:c,body:JSON.stringify({name:a,input:o})})).json()},T=(e,t)=>n(async n=>{let r=await v(n,t),i=r?.sub??``,a=r?.path??``,o=t.lobby??``;if(r||!o){if(!i)return n.text(`Unauthorized`,401);if(o=await(t.own??(()=>``))({env:c(n),sub:i,path:a}),!o)return n.text(`Forbidden`,403)}let s=new d({name:t.name??`aiax`,version:t.version??`1.0.0`}),l=new u;return e.actions.forEach((e,r)=>s.registerTool(r,{description:e.description,inputSchema:{...e.input}},async e=>{let s=await w(c(n)[t.binding??`v1`],o,{sub:i,path:a},r,e);return{content:[{type:`text`,text:JSON.stringify(s)}]}})),await s.connect(l),l.handleRequest(n)}),E=async(n,r,i,a)=>{let o=new Headers(n.req.raw.headers);o.set(t,i),o.set(e,encodeURIComponent(a));let s=new URL(n.req.url),l=s.pathname.split(`/`);l[3]=encodeURIComponent(r),s.pathname=l.join(`/`);let u=new Request(s,new Request(n.req.raw,{headers:o}));return await p(u,c(n))??n.text(`Not Found`,404)},D=e=>n(async t=>{let n=decodeURIComponent(t.req.path.split(`/`)[3]??``),r=t.req.query(`path`)??``,i=await g(t);if(e.lobby&&n===e.lobby)return E(t,n,i,r);if(!i)return t.text(`Unauthorized`,401);let a=await(e.own??(()=>``))({env:c(t),sub:i,path:r});return!a||n!==a&&n!==(e.alias??`my-room`)?t.text(`Forbidden`,403):E(t,a,i,r)}),O=(e,t={})=>{let r=Object.assign(e.config,t),i={auth:h(r),party:D(r),mcp:T(e,r),llm:C(r)};return n(async(e,t)=>{let n=()=>e.req.path.startsWith(`/parties/`)?i.party(e,t):e.req.path===(r.endpoint??`/api/mcp`)?i.mcp(e,t):e.req.path===(r.relay??`/api/llm`)&&e.req.method===`POST`?i.llm(e,t):t(),a;if(await i.auth(e,async()=>void(a=await n())),a)return a})};export{O as hono};
package/dist/index.d.ts CHANGED
@@ -1,16 +1,7 @@
1
- import { Config, Own } from "./types.js";
2
- //#region src/worker/auth.d.ts
3
- declare const ownSite: (bucket: (env: any) => any, meta?: string) => Own;
4
- declare const ownSelf: () => Own;
5
- //#endregion
1
+ import { ActionDef, Ai, Config, Cookie, Define, Grant, Own, Peer, Saved, Side } from "./types.js";
6
2
  //#region src/index.d.ts
7
- declare const createAiax: (config: Config) => {
8
- config: Config;
9
- all: () => import("hono").MiddlewareHandler<any, string, {}, Response>;
10
- auth: () => import("hono").MiddlewareHandler;
11
- party: () => import("hono").MiddlewareHandler<any, string, {}, any>;
12
- mcp: () => import("hono").MiddlewareHandler<any, string, {}, Response>;
13
- llm: () => import("hono").MiddlewareHandler<any, string, {}, Response>;
14
- };
3
+ declare const side: Side;
4
+ declare function x<P extends object>(params: P, config?: Config): Ai<P>;
5
+ declare function x<P extends object = {}>(params?: Partial<P>, config?: Config): Ai<P>;
15
6
  //#endregion
16
- export { createAiax, ownSelf, ownSite };
7
+ export { type ActionDef, type Ai, type Config, type Cookie, type Define, type Grant, type Own, type Peer, type Saved, type Side, side, x };
package/dist/index.js CHANGED
@@ -1,2 +1 @@
1
- import{PATH_HEADER as e,SUB_HEADER as t}from"./const.js";import{actions as n}from"./action.js";import{createMiddleware as r}from"hono/factory";import i from"@auth/core/providers/google";import{decode as a,encode as o}from"@auth/core/jwt";import{DrizzleAdapter as s}from"@auth/drizzle-adapter";import{getAuthUser as c,initAuthConfig as l}from"@hono/auth-js";import{drizzle as u}from"drizzle-orm/d1";import{env as d}from"hono/adapter";import{streamText as f}from"hono/streaming";import{StreamableHTTPTransport as p}from"@hono/mcp";import{McpServer as m}from"@modelcontextprotocol/sdk/server/mcp.js";import{getServerByName as h,routePartykitRequest as g}from"partyserver";const _=`authjs.mcp-token`,v=e=>l(t=>{let n=new URL(t.req.url).host,r=!!e.cookie&&e.cookie.services.some(t=>n===`${t}.${e.cookie.apex}`),a={sessionToken:{options:{domain:`.${e.cookie?.apex}`}}},o=e.providers??[i({clientId:t.env.GOOGLE_CLIENT_ID,clientSecret:t.env.GOOGLE_CLIENT_SECRET})];return{...e.database?{adapter:s(u(e.database(t.env)))}:{},providers:o,secret:t.env.AUTH_SECRET,session:{strategy:`jwt`},...r?{cookies:a}:{}}}),y=async e=>(await c(e))?.token?.sub??``,b=(e,t,n,r)=>o({token:{sub:t,path:n},secret:d(e).AUTH_SECRET,salt:r?.salt??_,maxAge:r?.maxAge??43200}),x=async(e,t)=>{let n=(e.req.header(`Authorization`)??``).replace(/^Bearer\s+/i,``);if(!n)return null;let r=await a({token:n,secret:d(e).AUTH_SECRET,salt:t?.salt??_}).catch(()=>null);return{sub:r?.sub??``,path:r?.path??``}},S=(e,t=`site`)=>async(n,r,i)=>!r||!i?``:(await e(n).head(`${r}/${i}`))?.customMetadata?.[t]??``,C=()=>(e,t)=>t,w=e=>e.startsWith(`data:`)?JSON.parse(e.slice(5)):null,T=(e,t,n)=>fetch(e.api,{method:`POST`,headers:{"x-api-key":e.key,"anthropic-version":`2023-06-01`,"anthropic-beta":`mcp-client-2025-11-20`,"content-type":`application/json`},body:JSON.stringify({model:e.model,max_tokens:e.maxTokens,stream:!0,cache_control:{type:`ephemeral`},system:e.system,mcp_servers:[{type:`url`,url:e.url,name:e.name,...t?{authorization_token:t}:{}}],tools:[{type:`mcp_toolset`,mcp_server_name:e.name}],messages:n})}),E=async(e,t)=>{let n=[],r=null,i=0,a=0,o=0,s=``,c=e.body.getReader(),l=new TextDecoder,u=``;for(;;){let{done:e,value:d}=await c.read();if(e)break;u+=l.decode(d,{stream:!0});let f=u.split(`
2
- `);u=f.pop()??``;for(let e of f){let c=w(e);if(c){if(c.type===`message_start`){let e=c.message?.usage??{};i+=(e.input_tokens??0)+(e.cache_creation_input_tokens??0)+(e.cache_read_input_tokens??0),a+=e.cache_read_input_tokens??0}c.type===`message_delta`&&(o=c.usage?.output_tokens??o,s=c.delta?.stop_reason??s),c.type===`content_block_start`&&(r={...c.content_block},n.push(r),r.type===`mcp_tool_use`&&await t(`\n[tool] ${r.server_name} ${r.name}\n`)),c.type===`content_block_delta`&&c.delta?.type===`text_delta`&&(r.text=(r.text??``)+c.delta.text,await t(c.delta.text)),c.type===`content_block_delta`&&c.delta?.type===`input_json_delta`&&(r.partial=(r.partial??``)+c.delta.partial_json),c.type===`content_block_delta`&&c.delta?.type===`thinking_delta`&&(r.thinking=(r.thinking??``)+c.delta.thinking),c.type===`content_block_delta`&&c.delta?.type===`signature_delta`&&(r.signature=(r.signature??``)+c.delta.signature),c.type===`content_block_stop`&&r?.partial!==void 0&&(r.input=r.partial?JSON.parse(r.partial):{},delete r.partial)}}}return{blocks:n,input:i,cached:a,output:o,stop:s}},D=async(e,t,n,r,i,a=10)=>{let o=r,s=0,c=0,l=0,u=``;for(let r=0;r<a;r++){let r=await E(o,i);if(s+=r.input,c+=r.cached,l+=r.output,u=r.stop||u,u!==`pause_turn`)break;if(n.push({role:`assistant`,content:r.blocks.filter(e=>e.type!==`thinking`||e.thinking||e.signature)}),o=await T(e,await t(),n),!o.ok){await i(`\n[error] ${(await o.text()).replace(/\n/g,` `)}\n`);break}}await i(`\n[usage] input ${s} (cache ${c}) / output ${l} tokens (${u})\n`)},O=e=>r(async t=>{let n=await y(t),{path:r=``,prompt:i,model:a=``,messages:o=[]}=await t.req.json(),s=!!e.lobby&&!r;if(!s&&!n)return t.text(`Unauthorized`,401);if(!s&&!await e.own(d(t),n,r))return t.text(`Forbidden`,403);let c=t.req.header(`x-original-host`)??t.req.header(`x-forwarded-host`)??new URL(t.req.url).host,l={key:d(t).ANTHROPIC_API_KEY,api:e.claude??`https://api.anthropic.com/v1/messages`,name:e.name,url:`https://${c}${e.endpoint??`/api/mcp`}`,model:a||e.model||`claude-sonnet-5`,system:e.system??``,maxTokens:e.maxTokens??32e3},u=()=>s?Promise.resolve(``):b(t,n,r,e),p=[...o,{role:`user`,content:i}],m=await T(l,await u(),p);return m.ok?f(t,t=>D(l,u,p,m,e=>t.write(e),e.maxTurns??10)):t.text(await m.text(),500)}),k=async(n,r,i,a,o)=>{let s=await h(n,r),c={[t]:i.sub,[e]:encodeURIComponent(i.path)};return(await s.fetch(`https://internal/act`,{method:`POST`,headers:c,body:JSON.stringify({name:a,input:o})})).json()},A=e=>r(async t=>{let r=await x(t,e),i=r?.sub??``,a=r?.path??``,o=e.lobby??``;if(r||!o){if(!i)return t.text(`Unauthorized`,401);if(o=await e.own(d(t),i,a),!o)return t.text(`Forbidden`,403)}let s=new m({name:e.name,version:e.version??`1.0.0`}),c=new p;return n.forEach((n,r)=>s.registerTool(r,{description:n.description,inputSchema:{...n.input}},async n=>{let s=await k(d(t)[e.binding??`v1`],o,{sub:i,path:a},r,n);return{content:[{type:`text`,text:JSON.stringify(s)}]}})),await s.connect(c),c.handleRequest(t)}),j=async(n,r,i,a)=>{let o=new Headers(n.req.raw.headers);o.set(t,i),o.set(e,encodeURIComponent(a));let s=new URL(n.req.url),c=s.pathname.split(`/`);c[3]=encodeURIComponent(r),s.pathname=c.join(`/`);let l=new Request(s,new Request(n.req.raw,{headers:o}));return await g(l,d(n))??n.text(`Not Found`,404)},M=e=>r(async t=>{let n=decodeURIComponent(t.req.path.split(`/`)[3]??``),r=t.req.query(`path`)??``,i=await y(t);if(e.lobby&&n===e.lobby)return j(t,n,i,r);if(!i)return t.text(`Unauthorized`,401);let a=await e.own(d(t),i,r);return!a||n!==a&&n!==(e.alias??`my-room`)?t.text(`Forbidden`,403):j(t,a,i,r)}),N=e=>{let t={auth:v(e),party:M(e),mcp:A(e),llm:O(e)},n=r(async(n,r)=>{let i=()=>n.req.path.startsWith(`/parties/`)?t.party(n,r):n.req.path===(e.endpoint??`/api/mcp`)?t.mcp(n,r):n.req.path===`/api/llm`&&n.req.method===`POST`?t.llm(n,r):r(),a;if(await t.auth(n,async()=>void(a=await i())),a)return a});return{config:e,all:()=>n,auth:()=>t.auth,party:()=>t.party,mcp:()=>t.mcp,llm:()=>t.llm}};export{N as createAiax,C as ownSelf,S as ownSite};
1
+ import*as e from"yjs";const t=(t,n)=>{let r=t.ui??[],i=e=>r.some(t=>t.endsWith(`/`)?e.startsWith(t):e===t),a=new WeakMap,o=e=>{for(let t of e.changed.values())for(let e of t)if(e===null||!i(e))return!0;return!1};return{rows(e){let t=[];return n().forEach((n,r)=>r.startsWith(e)&&!r.slice(e.length).includes(`/`)&&t.push(n)),t.sort((e,t)=>(e?.pos??0)-(t?.pos??0))},history(r){let i=n();return a.has(i)||a.set(i,new e.UndoManager(i,{_captureTransaction:o,...t.history,...r})),a.get(i)},snapshot(){let t=new e.Doc,r=t.getMap(`tree`);return t.transact(()=>n().forEach((t,n)=>!i(n)&&r.set(n,t instanceof e.AbstractType?t.clone():t))),e.encodeStateAsUpdate(t)}}},n=(t,n)=>{let r=new e.Doc,i=new WeakSet,a=new WeakMap,o=new Map,s=(e,c)=>{e&&(r=e),c&&a.set(r,c);let l=r;if(i.has(l))return;i.add(l);let u=l.getMap(`act-req`),d=l.getMap(`act-res`),f=async e=>{let r=u.get(e),i=n.get(r?.name);if(!i||i.side!==t)return;u.delete(e);let o=a.get(l)??(e=>e());d.set(e,{value:await o(()=>(s(l),i.run(r.input)))})},p=e=>{let t=o.get(e);!t||!d.has(e)||(o.delete(e),t(d.get(e).value),d.delete(e))};u.observe(e=>e.keysChanged.forEach(f)),d.observe(e=>e.keysChanged.forEach(p))};return{open:s,data:(e=`tree`)=>r.getMap(e),call(e,i){let a=n.get(e);return a?a.side===t||a.side===`both`?(s(),Promise.resolve(a.run(i))):new Promise(t=>{let n=crypto.randomUUID();o.set(n,t),r.getMap(`act-req`).set(n,{name:e,input:i})}):Promise.reject(Error(`unknown action: ${e}`))}}},r=e=>!!e&&typeof e.parse==`function`,i=e=>r(e)?{value:e}:e,a=(e,t)=>r(e)&&t&&typeof t==`object`&&`value`in t?t.value:t,o=(e,t,n)=>{let r=[],o=(e,r)=>i=>{let a=t.get(e);return a?a(i,r,n()):r(i)};return{chain:o,missing:e=>()=>Promise.reject(Error(`not implemented: ${e}`)),describe(e,t){return t?(r=[...r,e],t(n()),r=r.slice(0,-1),n()):n()},define:t=>(s,...c)=>{let l=c.find(e=>typeof e==`string`)??``,u=c.find(e=>typeof e==`function`),d=c.find(e=>e&&typeof e==`object`),f=async e=>{let t=a(d,e),r=await u(t,n());return typeof r==`function`?r(t,n()):r&&r.default?r.default(t,n()):r},p=[...r,l].filter(Boolean).join(` `);return e.set(s,{name:s,description:p,side:t,input:i(d),run:o(s,f)}),n()}}},s=typeof window>`u`?`server`:`client`;function c(e={},r={}){let i=new Map,a=new Map,c=new Set,l=n(s,i),u=t(r,l.data),d=o(i,a,()=>m),f=async()=>{let e={};return l.data().forEach((t,n)=>e[n]=t),e},p={config:r,side:s,actions:i,action:d.define(`both`),client:d.define(`client`),server:d.define(`server`),describe:d.describe,abstract:e=>(c.add(e),m),override:(e,t)=>(a.set(e,t),m),data:l.data,open:(e,t)=>(l.open(e,t),m),call:l.call,rows:u.rows,scan:e=>{let t=[];return l.data().forEach((n,r)=>r.startsWith(e)&&t.push(n)),t},wipe:e=>{let t=[];l.data().forEach((n,r)=>r.startsWith(e)&&t.push(r)),t.forEach(e=>l.data().delete(e))},tx:(e,t)=>l.data().doc.transact(e,t),history:u.history,snapshot:u.snapshot};i.set(`get_tree`,{name:`get_tree`,description:r.tree??`Dump every key/value of the shared tree to observe the current state before acting`,side:`both`,run:d.chain(`get_tree`,f)});let m=new Proxy(p,{get(t,n){if(typeof n!=`string`||n in t)return t[n];if(i.has(n))return e=>l.call(n,e);if(c.has(n))return d.chain(n,d.missing(n));if(n!==`then`&&n!==`toJSON`)return l.data().get(n)??e[n]},set(e,t,n){return typeof t!=`string`||t in e?(e[t]=n,!0):(l.data().set(t,n),!0)},deleteProperty(e,t){return typeof t!=`string`||t in e||l.data().delete(t),!0}});return m}export{s as side,c as x};
@@ -0,0 +1,23 @@
1
+ import { t as Route } from "./client-DG7hCEbP.js";
2
+ import { ReactNode } from "react";
3
+ //#region src/react.d.ts
4
+ declare const atom: <T>(initial: T) => {
5
+ value: T;
6
+ listeners: Set<(v: T) => void>;
7
+ };
8
+ type Atom<T> = ReturnType<typeof atom<T>>;
9
+ declare const setAtom: <T>(a: Atom<T>, v: T | ((p: T) => T)) => void;
10
+ declare const useAtomValue: <T>(a: Atom<T>) => T;
11
+ declare const useAtom: <T>(a: Atom<T>) => [T, (v: T) => void];
12
+ declare const AtomValue: <T>({ atom, children }: {
13
+ atom: Atom<T>;
14
+ children: (v: T) => ReactNode;
15
+ }) => ReactNode;
16
+ declare const useClient: <T extends object>(ai: T, route?: Route) => T;
17
+ declare const Client: <T extends object>({ ai, route, children }: {
18
+ ai: T;
19
+ route?: Route;
20
+ children: (ai: T) => ReactNode;
21
+ }) => ReactNode;
22
+ //#endregion
23
+ export { Atom, AtomValue, Client, atom, setAtom, useAtom, useAtomValue, useClient };
package/dist/react.js ADDED
@@ -0,0 +1 @@
1
+ import{useCallback as e,useEffect as t,useState as n}from"react";const r=e=>({value:e,listeners:new Set}),i=(e,t)=>{typeof t==`function`&&(t=t(e.value)),e.value=t,e.listeners.forEach(e=>e(t))},a=e=>{let[r,i]=n(()=>e.value);return t(()=>{let t=e=>i(e);return e.listeners.add(t),r!==e.value&&i(e.value),()=>void e.listeners.delete(t)},[e]),r},o=t=>[a(t),e(e=>i(t,e),[t])],s=({atom:e,children:t})=>t(a(e)),c=(e,r)=>{let[,i]=n(0);return t(()=>{import(`./client.js`).then(({client:t})=>t(e,r));let t=e.data(),n=()=>i(e=>e+1);return t.observe(n),()=>t.unobserve(n)},[e]),e},l=({ai:e,route:t,children:n})=>n(c(e,t));export{s as AtomValue,l as Client,r as atom,i as setAtom,o as useAtom,a as useAtomValue,c as useClient};
package/dist/server.d.ts CHANGED
@@ -1,14 +1,65 @@
1
- import { Grant } from "./types.js";
2
- import { Call } from "./action.js";
3
- import { BINDING } from "./const.js";
1
+ import { ActionDef, Ai, Config, Define, Grant, Side } from "./types.js";
2
+ import * as Y from "yjs";
4
3
  import { YServer } from "y-partyserver";
5
4
  //#region src/server.d.ts
6
- declare class AiaxServer extends YServer {
5
+ declare class Server extends YServer {
6
+ static options: {
7
+ hibernate: boolean;
8
+ };
9
+ static callbackOptions: {
10
+ debounceWait: number;
11
+ debounceMaxWait: number;
12
+ };
13
+ env: any;
14
+ ctx: any;
15
+ ai: Ai;
16
+ sub: string;
17
+ path: string;
18
+ grant: Grant | null;
19
+ queue: Promise<any>;
20
+ hook: (run: () => Promise<any>) => Promise<any>;
21
+ wire: () => {
22
+ config: Config;
23
+ side: Side;
24
+ actions: Map<string, ActionDef>;
25
+ action: Define<{}>;
26
+ client: Define<{}>;
27
+ server: Define<{}>;
28
+ describe(text: string): /*elided*/ any;
29
+ describe<U extends object>(text: string, fn: (ai: /*elided*/ any) => Ai<U>): Ai<U>;
30
+ abstract<N extends string>(name: N): Ai<Record<N, (input?: any) => any>>;
31
+ override(name: string, fn: (input: any, next: (input: any) => Promise<any>, ai: any) => any): /*elided*/ any;
32
+ data<V = any>(name?: string): Y.Map<V>;
33
+ open(doc?: Y.Doc, wrap?: (run: () => Promise<any>) => Promise<any>): /*elided*/ any;
34
+ call(name: string, input?: any): Promise<any>;
35
+ rows<V = any>(prefix: string): V[];
36
+ scan<V = any>(prefix: string): V[];
37
+ wipe(prefix: string): void;
38
+ tx(fn: () => void, origin?: any): void;
39
+ history(options?: object): Y.UndoManager;
40
+ snapshot(): Uint8Array;
41
+ };
7
42
  wrap: (run: () => Promise<any>) => Promise<any>;
8
- call: Call;
43
+ call: (name: string, input?: any) => Promise<any>;
44
+ conf(): Config;
45
+ isLobby(): boolean;
46
+ saved(grant: Grant): {
47
+ env: any;
48
+ sub: string;
49
+ path: string;
50
+ room: string;
51
+ };
9
52
  grantOf(request: Request): Grant;
10
- onAct(_grant: Grant, _request: Request): Promise<void>;
53
+ owner(): Promise<Grant>;
54
+ remember(sub: string, path: string): Promise<void>;
55
+ onAct(grant: Grant, _request?: Request): Promise<void>;
56
+ onStart(): Promise<void>;
57
+ onConnect(conn: any, ctx: any): Promise<void>;
58
+ onLoad(): Promise<void>;
59
+ onSave(): Promise<void>;
60
+ onAlarm(): Promise<void>;
61
+ onClose(conn: any, code: number, reason: string, wasClean: boolean): Promise<void>;
11
62
  onRequest(request: Request): Promise<Response>;
12
63
  }
13
64
  //#endregion
14
- export { AiaxServer, BINDING, type Grant };
65
+ export { Server };
package/dist/server.js CHANGED
@@ -1 +1 @@
1
- import{BINDING as e}from"./const.js";import{actions as t,bind as n,init as r}from"./action.js";import{YServer as i}from"y-partyserver";var a=class extends i{wrap=e=>e();call=r(this.document,e=>this.wrap(e));grantOf(e){return{sub:e.headers.get(`x-user-sub`)??``,path:decodeURIComponent(e.headers.get(`x-user-path`)??``)}}async onAct(e,t){}async onRequest(e){if(n(this.document),e.method!==`POST`)return new Response(`Not Found`,{status:404});let{name:r,input:i}=await e.json();return t.get(r)?.side===`client`?Response.json({ok:!0,result:await this.call(r,i)}):(await this.onAct(this.grantOf(e),e),Response.json({ok:!0,result:await this.wrap(()=>this.call(r,i))}))}};export{a as AiaxServer,e as BINDING};
1
+ import"./const.js";import*as e from"yjs";import{YServer as t}from"y-partyserver";var n=class extends t{static options={hibernate:!0};static callbackOptions={debounceWait:6e4,debounceMaxWait:6e4};ai=null;sub=``;path=``;grant=null;queue=Promise.resolve();hook=e=>e();wire=()=>this.ai.open(this.document,e=>this.wrap(e));wrap=e=>{let t=this.grant;this.grant=null;let n=async()=>{let n={env:this.env,room:this.name,...t??await this.owner()};return this.wire(),this.ai.override(`peer`,()=>n),this.hook(e)};return this.queue=this.queue.then(n,n),this.queue};call=(e,t)=>(this.wire(),this.ai.call(e,t));conf(){return this.ai?.config??{}}isLobby(){let e=this.conf().lobby;return!!e&&this.name===e}saved(e){return{env:this.env,sub:e.sub,path:e.path,room:this.name}}grantOf(e){return{sub:e.headers.get(`x-user-sub`)??``,path:decodeURIComponent(e.headers.get(`x-user-path`)??``)}}async owner(){for(let e of this.getConnections()){let t=e.state;if(t?.sub)return t}return this.sub?{sub:this.sub,path:this.path}:await this.ctx.storage.get(`owner`)??{sub:``,path:``}}async remember(e,t){this.sub=e||this.sub,this.path=t||this.path,e&&await this.ctx.storage.put(`owner`,{sub:e,path:t})}async onAct(e,t){this.grant=e,await this.remember(e.sub,e.path)}async onStart(){this.wire(),await super.onStart(),this.conf().save&&this.document.on(`update`,()=>{this.isLobby()||this.ctx.storage.setAlarm(Date.now()+7e4)})}async onConnect(e,t){let{sub:n,path:r}=this.grantOf(t.request);return e.setState({sub:n,path:r}),await this.remember(n,r),super.onConnect(e,t)}async onLoad(){this.wire();let t=this.conf().load;if(!t||this.isLobby())return;let n=await t(this.saved(await this.owner()));n&&n.byteLength&&e.applyUpdate(this.document,new Uint8Array(n),`load`)}async onSave(){let e=this.conf().save;if(!e||this.isLobby())return;let t=await this.owner();t.sub&&(this.wire(),await e(this.saved(t),this.ai.snapshot()))}async onAlarm(){await this.onSave()}async onClose(e,t,n,r){await super.onClose(e,t,n,r),await this.onSave()}async onRequest(e){if(this.wire(),e.method!==`POST`)return new Response(`Not Found`,{status:404});let{name:t,input:n}=await e.json();return this.ai.actions.get(t)?.side===`client`?Response.json({ok:!0,result:await this.call(t,n)}):(await this.onAct(this.grantOf(e),e),Response.json({ok:!0,result:await this.wrap(()=>this.call(t,n))}))}};export{n as Server};
package/dist/types.d.ts CHANGED
@@ -1,28 +1,36 @@
1
- import { ZodRawShape } from "zod";
1
+ import * as Y from "yjs";
2
+ import { ZodRawShape, ZodType } from "zod";
2
3
  //#region src/types.d.ts
3
4
  type Side = 'server' | 'client' | 'both';
4
- interface ActMeta {
5
+ interface ActionDef {
5
6
  name: string;
6
7
  description: string;
7
- input?: ZodRawShape;
8
- }
9
- interface ActionDef extends ActMeta {
10
8
  side: Side;
9
+ input?: ZodRawShape;
11
10
  run: (input: any) => Promise<any>;
12
11
  }
13
12
  interface Grant {
14
13
  sub: string;
15
14
  path: string;
16
15
  }
17
- type Own = (env: any, sub: string, path: string) => Promise<string> | string;
16
+ interface Peer {
17
+ env: any;
18
+ sub: string;
19
+ path: string;
20
+ }
21
+ interface Saved extends Peer {
22
+ room: string;
23
+ }
24
+ type Own = (at: Peer) => Promise<string> | string;
18
25
  interface Cookie {
19
26
  apex: string;
20
27
  services: string[];
21
28
  }
22
29
  interface Config {
23
- name: string;
24
- own: Own;
25
- database?: (env: any) => any;
30
+ name?: string;
31
+ tree?: string;
32
+ own?: Own;
33
+ adapter?: (env: any) => any;
26
34
  lobby?: string;
27
35
  alias?: string;
28
36
  system?: string;
@@ -31,12 +39,54 @@ interface Config {
31
39
  maxTokens?: number;
32
40
  maxTurns?: number;
33
41
  binding?: string;
42
+ host?: string;
34
43
  endpoint?: string;
44
+ relay?: string;
35
45
  version?: string;
36
46
  salt?: string;
37
47
  maxAge?: number;
38
48
  cookie?: Cookie;
39
49
  providers?: any[];
50
+ ui?: string[];
51
+ history?: object;
52
+ load?: (at: Saved) => Promise<any>;
53
+ save?: (at: Saved, buf: Uint8Array) => Promise<any>;
40
54
  }
55
+ type AnyFn = (...args: any[]) => any;
56
+ type Shape = ZodRawShape | ZodType;
57
+ type Value<Z> = Z extends ZodType<infer O> ? O : any;
58
+ type Opt<S> = { [K in keyof S]: undefined extends Value<S[K]> ? K : never; }[keyof S];
59
+ type Input<S> = S extends ZodType<infer O> ? O : { [K in Exclude<keyof S, Opt<S>>]: Value<S[K]>; } & { [K in Opt<S>]?: Value<S[K]>; };
60
+ type Result<F> = F extends AnyFn ? (Awaited<ReturnType<F>> extends {
61
+ default: infer D;
62
+ } ? (D extends AnyFn ? Awaited<ReturnType<D>> : Awaited<ReturnType<F>>) : Awaited<ReturnType<F>>) : never;
63
+ type Part = string | Shape | AnyFn;
64
+ type FnOf<A> = A extends readonly [infer H, ...infer R] ? (H extends AnyFn ? H : FnOf<R>) : never;
65
+ type SchemaOf<A> = A extends readonly [infer H, ...infer R] ? (H extends AnyFn ? SchemaOf<R> : H extends string ? SchemaOf<R> : H) : unknown;
66
+ type Tool<S, F> = unknown extends S ? (input?: any) => Promise<Result<F>> : (input: Input<S>) => Promise<Result<F>>;
67
+ interface Define<T extends object> {
68
+ <N extends string, A extends readonly [Part, ...Part[]]>(name: N, ...parts: A): Ai<T & Record<N, Tool<SchemaOf<A>, FnOf<A>>>>;
69
+ }
70
+ type Ai<T extends object = {}> = T & {
71
+ config: Config;
72
+ side: Side;
73
+ actions: Map<string, ActionDef>;
74
+ action: Define<T>;
75
+ client: Define<T>;
76
+ server: Define<T>;
77
+ describe(text: string): Ai<T>;
78
+ describe<U extends object>(text: string, fn: (ai: Ai<T>) => Ai<U>): Ai<T & U>;
79
+ abstract<N extends string>(name: N): Ai<T & Record<N, (input?: any) => any>>;
80
+ override(name: string, fn: (input: any, next: (input: any) => Promise<any>, ai: any) => any): Ai<T>;
81
+ data<V = any>(name?: string): Y.Map<V>;
82
+ open(doc?: Y.Doc, wrap?: (run: () => Promise<any>) => Promise<any>): Ai<T>;
83
+ call(name: string, input?: any): Promise<any>;
84
+ rows<V = any>(prefix: string): V[];
85
+ scan<V = any>(prefix: string): V[];
86
+ wipe(prefix: string): void;
87
+ tx(fn: () => void, origin?: any): void;
88
+ history(options?: object): Y.UndoManager;
89
+ snapshot(): Uint8Array;
90
+ };
41
91
  //#endregion
42
- export { ActMeta, ActionDef, Config, Cookie, Grant, Own, Side };
92
+ export { ActionDef, Ai, Config, Cookie, Define, Grant, Own, Peer, Saved, Side };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ai-ax",
3
3
  "author": "tseijp",
4
- "version": "0.4.3",
4
+ "version": "0.6.0",
5
5
  "license": "MIT",
6
6
  "description": "AI Agent Experience (AI/AX) library for Hono + Cloudflare. One act surface for both human UI/UX and LLM MCP tools, with authentication, authorization, realtime sync and Claude API relay.",
7
7
  "keywords": [
@@ -30,10 +30,6 @@
30
30
  "types": "./dist/index.d.ts",
31
31
  "default": "./dist/index.js"
32
32
  },
33
- "./action": {
34
- "types": "./dist/action.d.ts",
35
- "default": "./dist/action.js"
36
- },
37
33
  "./client": {
38
34
  "types": "./dist/client.d.ts",
39
35
  "default": "./dist/client.js"
@@ -42,10 +38,22 @@
42
38
  "types": "./dist/const.d.ts",
43
39
  "default": "./dist/const.js"
44
40
  },
41
+ "./hono": {
42
+ "types": "./dist/hono.d.ts",
43
+ "default": "./dist/hono.js"
44
+ },
45
+ "./react": {
46
+ "types": "./dist/react.d.ts",
47
+ "default": "./dist/react.js"
48
+ },
45
49
  "./server": {
46
50
  "types": "./dist/server.d.ts",
47
51
  "default": "./dist/server.js"
48
52
  },
53
+ "./types": {
54
+ "types": "./dist/types.d.ts",
55
+ "default": "./dist/types.js"
56
+ },
49
57
  "./src": {
50
58
  "types": "./src/index.ts",
51
59
  "default": "./src/index.ts"
@@ -61,40 +69,48 @@
61
69
  "README.md"
62
70
  ],
63
71
  "scripts": {
64
- "build": "tsdown --config ../../tsdown.config.ts",
72
+ "build": "tsdown --config ../../tsdown.config.ts && rm -f dist/*-*.d.ts",
65
73
  "mmd": "for f in concept stack grant; do mmdc -i media/$f.mmd -o media/$f.svg; done"
66
74
  },
67
75
  "devDependencies": {
68
76
  "@auth/core": "0.34.3",
69
- "@auth/drizzle-adapter": "1.11.2",
70
77
  "@hono/auth-js": "1.1.1",
71
78
  "@hono/mcp": "0.3.1",
72
79
  "@modelcontextprotocol/sdk": "1.29.0",
73
- "drizzle-orm": "0.45.2",
80
+ "@types/react": "^19.2.17",
74
81
  "hono": "4.12.30",
75
82
  "partyserver": "0.5.8",
83
+ "react": "^19.2.7",
76
84
  "y-partyserver": "2.2.0",
77
85
  "yjs": "13.6.31",
78
86
  "zod": "4.4.3"
79
87
  },
80
88
  "peerDependencies": {
81
89
  "@auth/core": ">=0.34.0",
82
- "@auth/drizzle-adapter": ">=1.0.0",
83
90
  "@hono/auth-js": ">=1.0.0",
84
91
  "@hono/mcp": ">=0.3.0",
85
92
  "@modelcontextprotocol/sdk": ">=1.0.0",
86
- "drizzle-orm": ">=0.40.0",
87
93
  "hono": ">=4.0.0",
88
94
  "partyserver": ">=0.5.0",
95
+ "react": ">=18.0.0",
89
96
  "y-partyserver": ">=2.0.0",
90
97
  "yjs": ">=13.0.0",
91
98
  "zod": ">=3.25.0"
92
99
  },
93
100
  "peerDependenciesMeta": {
94
- "@auth/drizzle-adapter": {
101
+ "@hono/auth-js": {
102
+ "optional": true
103
+ },
104
+ "@hono/mcp": {
105
+ "optional": true
106
+ },
107
+ "@modelcontextprotocol/sdk": {
108
+ "optional": true
109
+ },
110
+ "hono": {
95
111
  "optional": true
96
112
  },
97
- "drizzle-orm": {
113
+ "react": {
98
114
  "optional": true
99
115
  }
100
116
  }
package/README.ja.md DELETED
@@ -1,147 +0,0 @@
1
- # ai-ax
2
-
3
- AI Agent Experience (AI/AX) library for Hono + Cloudflare.
4
-
5
- ai-ax は、web アプリの「人間向けの体験 (UI/UX)」と「AI agent 向けの体験 (AI/AX)」を 1 つの操作面として実装するための library。操作は 1 度だけ宣言され、その宣言が人間には click handler、LLM には MCP tool として同時に機能する。両者は同じ realtime 状態を読み書きする。
6
-
7
- library 全体を支える考えは 3 つ。**one action**、**one tree**、**one resolver**。
8
-
9
- ## Why
10
-
11
- web は「操作するのは人間」という前提で UI/UX を積み上げてきた。そこへ後から LLM の操作経路を足すと、操作面が 2 つ、認証認可が 2 系統、バグも 2 倍になる。2 つの実装は必ずずれていき、そのずれは数週間後の log でようやく見つかる。
12
-
13
- ai-ax は 2 つ目の実装を管理するのではなく、消す。AI agent をアプリのユーザーの一人として扱い、画面を見る人間と同じ状態を読ませ、同じ操作を呼ばせる。開発者が UI のボタンを click して debug すれば、その debug 済みコードがそのまま MCP 経由で agent が実行するコードになる。ずれていく「もう 1 つの実装」は存在しない。
14
-
15
- ![one action, one tree](https://r.tsei.jp/ai-ax/concept.svg)
16
-
17
- **one action** — 操作の最小単位を 1 度だけ宣言し、UI と MCP の両方から同じ実装に到達させる。**one tree** — 全状態は単一の yjs の木であり、人間には描画された画面として、LLM には `get_tree` の観測結果として同じ木が見える。**one resolver** — 認可は「誰がどの部屋に入れるか」を決める 1 つの関数に集約され、人間も AI もすべての経路がそこを通る。
18
-
19
- ## What
20
-
21
- 前提となる構成は Hono + Cloudflare Workers。realtime 同期は partyserver (Durable Objects) + yjs、認証は Auth.js、LLM は Claude API の MCP connector。ai-ax はその接続部 — 認証、認可、realtime 同期、MCP server、LLM 中継 — を Hono middleware として提供する。
22
-
23
- ![stack](https://r.tsei.jp/ai-ax/stack.svg)
24
-
25
- worker 側は合成 middleware を 1 つ置くだけでよく、Durable Object は re-export で済む。
26
-
27
- ```ts
28
- import { Hono } from 'hono'
29
- import { createAiax, ownSelf } from 'ai-ax'
30
- import { PROMPT } from './prompt'
31
- import './actions'
32
-
33
- const ai = createAiax({ name: 'demo', own: ownSelf(), database: (e) => e.my_d1, system: PROMPT })
34
- const app = new Hono().use('*', ai.all())
35
-
36
- export default app
37
- export { AiaxServer } from 'ai-ax/server'
38
- ```
39
-
40
- `ai.all()` は `/parties/*` を websocket 同期へ、`/api/mcp` を MCP server へ、`POST /api/llm` を Claude 中継へ振り分け、Auth.js の session も処理する。それ以外はすべて自作の route へ素通しする。export した `AiaxServer` は binding 名 `v1` の Durable Object として deploy する。
41
-
42
- ### one action
43
-
44
- ```ts
45
- import { action } from 'ai-ax/action'
46
- import { z } from 'zod'
47
-
48
- export const placeMark = action<{ cell: number }, string>(() => import('./actions/place-mark'), 'both', {
49
- name: 'place_mark',
50
- description: '空いているマスに現在の手番の印を置く',
51
- input: { cell: z.number().describe('マス番号 0-8') },
52
- })
53
- ```
54
-
55
- この 1 つの宣言から 2 つのものが生まれる。UI の event handler から呼べる async 関数と、LLM に公開される MCP tool (name / description / zod input schema)。第 1 引数は dynamic import でも実装関数そのものでもよく、bundle を分けたい service にも 1 file で書きたいアプリにも合う。
56
-
57
- `side` は「その実装がどこでなら動けるか」を表し、反対側から呼ばれたときは ai-ax が境界を越えて実行を届ける。
58
-
59
- | side | 動作場所 | 反対側から呼ばれたとき |
60
- | -------- | --------------------------------- | -------------------------------------------------- |
61
- | `both` | browser と Durable Object の両方 | 呼んだ側でそのまま実行 |
62
- | `client` | browser のみ (browser API が必要) | yjs 上の act-req / act-res で接続中 browser へ委譲 |
63
- | `server` | Durable Object のみ | Durable Object へ転送 |
64
-
65
- action の中では `data()` が共有の `Y.Map` (`tree`) を返す。ここへの書き込みは接続中の全 browser に realtime 同期され、LLM が観測するのも全く同じ木。だから agent の操作は人間の画面にその場で現れる。
66
-
67
- browser 側は 1 度接続すればすべてが動き出す。
68
-
69
- ```ts
70
- import { ALIAS, connect } from 'ai-ax/client'
71
-
72
- const link = connect({ room: ALIAS })
73
- link.synced(() => boot())
74
- ```
75
-
76
- client は常に別名 `my-room` で接続する。server が認可済みの room 名へ書き換えるため、client が実際の room 名を選ぶことはない。
77
-
78
- ### one resolver
79
-
80
- credential は 2 種類だけ。どちらも `AUTH_SECRET` で署名された jwt であり、平文の user id が network を渡ることはない。
81
-
82
- | 経路 | credential | 発行 | 検証 |
83
- | -------------------------- | ---------------------------- | --------------------------------- | ------------------------------------- |
84
- | Browser → Worker | session cookie (Auth.js jwt) | `/api/auth` (Google OAuth) | `userSub()` |
85
- | Claude → Worker `/api/mcp` | grant jwt `{ sub, path }` | llm middleware が認可後に発行 | `readGrant()` |
86
- | Worker → Durable Object | room 名 + `x-user-sub` | worker が認可済みの値で必ず上書き | DO へは worker binding 経由でのみ到達 |
87
-
88
- 認可は 1 つの関数に集約される。
89
-
90
- ```ts
91
- type Own = (env: any, sub: string, path: string) => Promise<string> | string
92
- ```
93
-
94
- 答えるのは「この user はこの path に対してどの部屋を得るか」だけで、空文字は拒否。`ownSelf()` は user 本人の id を返す (個人アプリ: 全員が自分だけの部屋を持つ)。`ownSite(bucket)` は R2 の file metadata を引く (editor アプリ: file を所有していればその部屋が開く)。D1 の所属 table を join する resolver も同じ型で書ける。
95
-
96
- grant jwt は llm middleware が turn ごとに発行し、Claude API がすべての MCP request に載せて `/api/mcp` へ運ぶ。MCP server は毎回署名を検証してから操作を実行する。LLM は token の中身を選べず、書き換えれば署名が壊れる。つまり agent はその瞬間に認可された 1 部屋の中に封じられる。
97
-
98
- ![grant sequence](https://r.tsei.jp/ai-ax/grant.svg)
99
-
100
- ### LLM 中継
101
-
102
- `POST /api/llm` に `{ path, prompt, model?, messages? }` を送ると、認可 → grant 発行 → MCP server 付きで Claude API 呼び出し → text stream 中継、が 1 本で流れる。`messages` は prompt の前に積む過去の会話。library は履歴を保存しない — 何をどこへどんな形で永続化するかはアプリの決めごとであり、ここに渡されたものをそのまま再生するだけ。
103
-
104
- stream は素の text で、`[tool] server name` の行が tool 呼び出しを、末尾の `[usage] ...` の行が turn の終わりを表す。この wire format の browser 側の相棒が `createStream()`。
105
-
106
- ```ts
107
- import { createStream } from 'ai-ax/client'
108
-
109
- const stream = createStream()
110
- const items = await stream.send({ prompt, path, model, messages }, (u) => draw(u.items))
111
- ```
112
-
113
- `parse` は raw text を `{ text, tools }` の item 列に分解して usage を隠し、`send` は POST しながら parse 済みの update を流す。対になる `pack` / `unpack` は完成した応答を保存可能な `{ reply, tools }` 形式と相互変換する。アプリは wire format そのものに触れずに、会話の描画・永続化・再生ができる。
114
-
115
- ## How
116
-
117
- | entry | 主な export | 動作場所 |
118
- | -------------- | ----------------------------------- | ------------------------ |
119
- | `ai-ax` | `createAiax`, `ownSelf`, `ownSite` | worker (Hono middleware) |
120
- | `ai-ax/client` | `connect`, `createStream`, `ALIAS` | browser |
121
- | `ai-ax/action` | `action`, `actions`, `data`, `side` | 共有の action 宣言 |
122
- | `ai-ax/server` | `AiaxServer` | Durable Object |
123
- | `ai-ax/const` | protocol 定数 (path と header 名) | 両方 |
124
-
125
- `createAiax(config)` は以下を受け取り `{ all, auth, party, mcp, llm }` を返す。`all()` が上で示した合成 middleware で、4 つの factory は個別にも使える。
126
-
127
- | key | 型 | 既定値 | 説明 |
128
- | --------- | --------------------- | ----------------------------------------- | ----------------------------------------------------------- |
129
- | name | `string` | 必須 | service id。MCP server 名になる |
130
- | own | `Own` | 必須 | 認可 resolver。`''` で拒否 |
131
- | database | `(env) => D1Database` | なし | Auth.js DrizzleAdapter に渡す D1。省略時は adapter なし jwt |
132
- | lobby | `string` | なし | 匿名で開放する demo 部屋 1 つ。省略時は全経路で認証必須 |
133
- | alias | `string` | `'my-room'` | 「自分の部屋」を指す接続名。`own` の結果へ書き換えられる |
134
- | system | `string` | `''` | LLM の system prompt |
135
- | model | `string` | `'claude-sonnet-5'` | Claude API の model id |
136
- | claude | `string` | `'https://api.anthropic.com/v1/messages'` | Claude API の URL。AI Gateway 経由に差し替え可 |
137
- | maxTokens | `number` | `32000` | 1 応答の max_tokens |
138
- | maxTurns | `number` | `10` | pause_turn 継続の上限回数 |
139
- | binding | `string` | `'v1'` | Durable Object binding 名 |
140
- | endpoint | `string` | `'/api/mcp'` | MCP endpoint の path |
141
- | version | `string` | `'1.0.0'` | MCP server の version 表記 |
142
- | salt | `string` | `'authjs.mcp-token'` | grant jwt の salt。service ごとの分離 |
143
- | maxAge | `number` | `43200` (12h) | grant jwt の有効秒数 |
144
- | cookie | `{ apex, services }` | なし | subdomain 間で session cookie を共有する service 群 |
145
- | providers | `any[]` | Google | Auth.js providers の差し替え |
146
-
147
- `x-user-sub` / `x-user-path` の header 名と anthropic-version は library 内部の protocol 定数であり、意図的に設定不可。protocol の両端は同時に変わる必要があるため。
package/dist/action.d.ts DELETED
@@ -1,13 +0,0 @@
1
- import { ActMeta, ActionDef, Side } from "./types.js";
2
- import * as Y from "yjs";
3
- //#region src/action.d.ts
4
- type Action<I, R> = (input: I) => R | Promise<R>;
5
- type Call = (name: string, input: any) => Promise<any>;
6
- declare const actions: Map<string, ActionDef>;
7
- declare const side: Side;
8
- declare const bind: (doc: Y.Doc) => Y.Doc;
9
- declare const data: <T = any>(name?: string) => Y.Map<T>;
10
- declare function action<I extends object, R>(load: Action<I, R> | ((input?: I) => any), at: Side, meta: ActMeta): (input: I) => Promise<R>;
11
- declare const init: (doc: Y.Doc, wrap?: (run: () => Promise<any>) => Promise<any>) => Call;
12
- //#endregion
13
- export { Call, action, actions, bind, data, init, side };
package/dist/action.js DELETED
@@ -1 +0,0 @@
1
- const e=new Map,t=typeof window>`u`?`server`:`client`;let n,r;const i=e=>r=e,a=(e=`tree`)=>r.getMap(e);function o(t,r,i){let a=async e=>{let n=await t(e);return typeof n==`function`?n(e):n?.default?n.default(e):n};return e.set(i.name,{...i,side:r,run:a}),e=>n(i.name,e)}const s=(r,a=e=>e())=>{i(r);let o=r.getMap(`act-req`),s=r.getMap(`act-res`),c=new Map,l=async n=>{let c=o.get(n),l=e.get(c?.name);!l||l.side!==t||(o.delete(n),s.set(n,{value:await a(()=>(i(r),l.run(c.input)))}))},u=e=>{let t=c.get(e);!t||!s.has(e)||(c.delete(e),t(s.get(e).value),s.delete(e))};return o.observe(e=>e.keysChanged.forEach(l)),s.observe(e=>e.keysChanged.forEach(u)),n=(n,a)=>{let s=e.get(n);return s.side===t||s.side===`both`?(i(r),s.run(a)):new Promise(e=>{let t=crypto.randomUUID();c.set(t,e),o.set(t,{name:n,input:a})})},n};export{o as action,e as actions,i as bind,a as data,s as init,t as side};