ai-ax 0.4.0 → 0.4.3
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.ja.md +147 -0
- package/README.md +113 -187
- package/dist/server.d.ts +2 -1
- package/dist/server.js +1 -1
- package/package.json +12 -6
package/README.ja.md
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
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/README.md
CHANGED
|
@@ -1,221 +1,147 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ai-ax
|
|
2
2
|
|
|
3
3
|
AI Agent Experience (AI/AX) library for Hono + Cloudflare.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
AX[AI/AX<br>LLM の tool 呼び出し] -->|同じ act| ACT
|
|
21
|
-
ACT <-->|読み書き| TREE[yjs Y.Map 'tree'<br>全状態 = 単一の木]
|
|
22
|
-
TREE -->|画面として| UI
|
|
23
|
-
TREE -->|get_tree として| AX
|
|
24
|
-
```
|
|
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.
|
|
6
|
+
|
|
7
|
+
Three ideas carry the whole library: **one action**, **one tree**, **one resolver**.
|
|
8
|
+
|
|
9
|
+
## Why
|
|
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.
|
|
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.
|
|
14
|
+
|
|
15
|
+

|
|
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.
|
|
18
|
+
|
|
19
|
+
## What
|
|
25
20
|
|
|
26
|
-
|
|
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.
|
|
27
22
|
|
|
28
|
-
|
|
23
|
+

|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
25
|
+
The worker needs one composite middleware, and the Durable Object is a 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
38
|
```
|
|
39
39
|
|
|
40
|
-
|
|
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`.
|
|
41
|
+
|
|
42
|
+
### One action
|
|
41
43
|
|
|
42
44
|
```ts
|
|
43
|
-
|
|
44
|
-
|
|
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: 'Place the current player mark on an empty cell',
|
|
51
|
+
input: { cell: z.number().describe('cell index 0-8') },
|
|
52
|
+
})
|
|
45
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
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
58
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
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 |
|
|
56
64
|
|
|
57
|
-
|
|
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.
|
|
66
|
+
|
|
67
|
+
The browser side connects once and everything above starts working.
|
|
58
68
|
|
|
59
69
|
```ts
|
|
60
|
-
|
|
70
|
+
import { ALIAS, connect } from 'ai-ax/client'
|
|
71
|
+
|
|
72
|
+
const link = connect({ room: ALIAS })
|
|
73
|
+
link.synced(() => boot())
|
|
61
74
|
```
|
|
62
75
|
|
|
63
|
-
|
|
64
|
-
所有の定義は service ごとに異なるため、resolver の実装は自由に差し替えられる。
|
|
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.
|
|
65
77
|
|
|
66
|
-
|
|
67
|
-
ownSite((env) => env.my_r2_xxx)
|
|
68
|
-
// R2 head(`${sub}/${path}`) の customMetadata.site が room。file を所有していれば editor の部屋が開く
|
|
78
|
+
### One resolver
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
// room = sub。認証した本人の部屋だけが開く。start page や個人 app 向け
|
|
80
|
+
Only two kinds of credentials exist, and both are jwt signed with `AUTH_SECRET`; a plain user id never crosses the network.
|
|
72
81
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
```
|
|
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 |
|
|
79
87
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
autonumber
|
|
85
|
-
participant Browser
|
|
86
|
-
participant Worker
|
|
87
|
-
participant Resolver as own resolver
|
|
88
|
-
participant ClaudeAPI
|
|
89
|
-
participant MCP as Worker /api/mcp
|
|
90
|
-
participant DO as Durable Object
|
|
91
|
-
|
|
92
|
-
Browser->>Worker: POST /api/llm { path, prompt } (session cookie)
|
|
93
|
-
Worker->>Resolver: own(env, sub, path)
|
|
94
|
-
alt room = '' (所有なし)
|
|
95
|
-
Worker-->>Browser: 401 / 403
|
|
96
|
-
else room あり
|
|
97
|
-
Worker->>ClaudeAPI: messages + mcp_servers<br>(authorization_token = grant jwt { sub, path })
|
|
98
|
-
ClaudeAPI->>MCP: MCP request (Authorization: Bearer grant)
|
|
99
|
-
MCP->>MCP: readGrant (復号失敗 / 期限切れ → 401)
|
|
100
|
-
MCP->>Resolver: own(env, sub, path)
|
|
101
|
-
alt room = ''
|
|
102
|
-
MCP-->>ClaudeAPI: 403
|
|
103
|
-
else room あり
|
|
104
|
-
MCP->>DO: getServerByName(binding, room) へ action 転送
|
|
105
|
-
DO-->>Browser: 実行結果を websocket で realtime 同期
|
|
106
|
-
DO-->>MCP: action の戻り値
|
|
107
|
-
MCP-->>ClaudeAPI: MCP response
|
|
108
|
-
ClaudeAPI-->>Worker: stream 応答
|
|
109
|
-
Worker-->>Browser: text stream 中継
|
|
110
|
-
end
|
|
111
|
-
end
|
|
88
|
+
Authorization concentrates into a single function.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
type Own = (env: any, sub: string, path: string) => Promise<string> | string
|
|
112
92
|
```
|
|
113
93
|
|
|
114
|
-
|
|
115
|
-
LLM は token の中身を選べず、書き換えれば署名が壊れる。
|
|
116
|
-
つまり LLM が操作できる部屋は「その瞬間に認可された 1 部屋」だけに封じられる。
|
|
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.
|
|
117
95
|
|
|
118
|
-
|
|
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.
|
|
119
97
|
|
|
120
|
-
|
|
98
|
+

|
|
121
99
|
|
|
122
|
-
|
|
123
|
-
- room 名を URL に持つ設計 (editor 型) では、URL の room と own の結果の一致を検査し、不一致は 403
|
|
124
|
-
- websocket・MCP どちらの経路でも x-user-sub / x-user-path header は worker が認可済みの値で上書きするため、外部から偽装できない
|
|
125
|
-
- lobby は唯一の例外。匿名で触れる demo 部屋を 1 つだけ開けられる。設定しなければ全経路で認証必須
|
|
100
|
+
### LLM relay
|
|
126
101
|
|
|
127
|
-
|
|
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.
|
|
128
103
|
|
|
129
|
-
|
|
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.
|
|
130
105
|
|
|
131
106
|
```ts
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
})
|
|
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))
|
|
137
111
|
```
|
|
138
112
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
/
|
|
147
|
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
|
|
|
160
|
-
|
|
|
161
|
-
|
|
|
162
|
-
|
|
|
163
|
-
|
|
|
164
|
-
|
|
|
165
|
-
|
|
|
166
|
-
|
|
|
167
|
-
|
|
|
168
|
-
|
|
|
169
|
-
|
|
|
170
|
-
|
|
|
171
|
-
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
| providers | any[] | Google | Auth.js providers の差し替え |
|
|
175
|
-
|
|
176
|
-
戻り値は `{ auth, party, mcp, llm }` の 4 middleware factory。
|
|
177
|
-
x-user-sub / x-user-path の header 名と anthropic-version は library 内部の protocol 定数であり、config では変えられない (両端が同時に変わる必要があるため)。
|
|
178
|
-
|
|
179
|
-
### 個別 export
|
|
180
|
-
|
|
181
|
-
| export | file | 説明 |
|
|
182
|
-
| ---------------------------------------------------------- | --------- | ------------------------------------------------------------------ |
|
|
183
|
-
| action / actions / init / serve / grantOf / side | act.ts | action system。宣言と登録簿、act-req/act-res の配線、DO 側の受け口 |
|
|
184
|
-
| auth / userSub / signGrant / readGrant / ownSite / ownSelf | auth.ts | 認証と認可の素材。Auth.js 設定、grant jwt、own preset |
|
|
185
|
-
| party | party.ts | websocket 経路の認可と room 書き換え・DO 転送 |
|
|
186
|
-
| mcp | mcp.ts | grant 検証・認可・action の MCP tool 化・DO への action 転送 |
|
|
187
|
-
| llm | llm.ts | Claude API 中継 stream (認可 → mint → converse) |
|
|
188
|
-
| callClaude / relay / converse | claude.ts | Claude API 呼び出しと stream の逐次処理・turn 継続 |
|
|
189
|
-
|
|
190
|
-
## 8. 適用パターン
|
|
191
|
-
|
|
192
|
-
[Hono and React realtime app](https://zenn.dev/jp/articles) の template がそのまま前提環境になる。
|
|
193
|
-
記事の authMiddleware / myMiddleware を ax.auth() / ax.party() に差し替え、/api/mcp と /api/llm の 2 行を足すと、realtime app が AI/AX 対応になる。
|
|
194
|
-
|
|
195
|
-
| 型 | own | lobby | room | 例 |
|
|
196
|
-
| ----------- | --------------- | ----- | ----------------- | -------------------- |
|
|
197
|
-
| 個人 app 型 | ownSelf() | なし | sub (本人の部屋) | projects/demo |
|
|
198
|
-
| editor 型 | ownSite(bucket) | 任意 | file の site id | editor services |
|
|
199
|
-
| group 型 | 自作 (D1 join) | 任意 | 所属 group の部屋 | 将来の collaboration |
|
|
200
|
-
|
|
201
|
-
## 9. これから — 初期化動線の吸収
|
|
202
|
-
|
|
203
|
-
Y.Doc の初期化は「browser で作る (公開閲覧の hydrate)」「DO で作る (draft の復元・demo の投入)」「空から作る (seed)」の 3 経路が排他で並び、ここが service 実装の最も複雑な場所になっている。
|
|
204
|
-
aiax は現時点で境界の契約 (認証認可・room 決定・action 転送) だけを持ち、初期化は service 側に残している。
|
|
205
|
-
次の段階で、実行場所 (client / worker / DO) と経路 (view / edit / lobby) の場合分けを宣言で表現する boot / persist API をこの library へ吸収する。
|
|
206
|
-
|
|
207
|
-
## 10. 攻撃面と防御
|
|
208
|
-
|
|
209
|
-
| 攻撃 | 防御 |
|
|
210
|
-
| ---------------------------------------------- | ------------------------------------------------------------------------ |
|
|
211
|
-
| MCP client が room 名を引数で自由指定する | tool 引数に room を持たせない。room は grant の sub/path から own が決定 |
|
|
212
|
-
| 他人の path で grant を得る | mint 前に own で所有を検査。mcp 側でも同じ own で再検査する二重化 |
|
|
213
|
-
| 期限切れ / 改竄された grant | AUTH_SECRET + salt 付き jwt。復号できなければ sub が空になり 401 |
|
|
214
|
-
| x-user-sub header の偽装 | worker が認可済みの値で必ず上書きしてから DO へ渡す |
|
|
215
|
-
| DO への直接アクセス | DO は worker binding 経由でのみ到達可能。公開 URL を持たない |
|
|
216
|
-
| 破壊的 action (公開データの書き換え) | action 実行の直前に own で再認可する実装を service 側で足せる |
|
|
217
|
-
| 認可の掏り替え (client が接続後に path を変更) | grant は { sub, path } 固定。別 path は別 grant の再発行が要る |
|
|
218
|
-
|
|
219
|
-
## License
|
|
220
|
-
|
|
221
|
-
MIT
|
|
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.
|
|
114
|
+
|
|
115
|
+
## How
|
|
116
|
+
|
|
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 |
|
|
146
|
+
|
|
147
|
+
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/server.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Grant } from "./types.js";
|
|
2
2
|
import { Call } from "./action.js";
|
|
3
|
+
import { BINDING } from "./const.js";
|
|
3
4
|
import { YServer } from "y-partyserver";
|
|
4
5
|
//#region src/server.d.ts
|
|
5
6
|
declare class AiaxServer extends YServer {
|
|
@@ -10,4 +11,4 @@ declare class AiaxServer extends YServer {
|
|
|
10
11
|
onRequest(request: Request): Promise<Response>;
|
|
11
12
|
}
|
|
12
13
|
//#endregion
|
|
13
|
-
export { AiaxServer, type Grant };
|
|
14
|
+
export { AiaxServer, BINDING, type Grant };
|
package/dist/server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./const.js";import{actions as
|
|
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};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-ax",
|
|
3
3
|
"author": "tseijp",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.3",
|
|
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": [
|
|
@@ -34,14 +34,18 @@
|
|
|
34
34
|
"types": "./dist/action.d.ts",
|
|
35
35
|
"default": "./dist/action.js"
|
|
36
36
|
},
|
|
37
|
-
"./server": {
|
|
38
|
-
"types": "./dist/server.d.ts",
|
|
39
|
-
"default": "./dist/server.js"
|
|
40
|
-
},
|
|
41
37
|
"./client": {
|
|
42
38
|
"types": "./dist/client.d.ts",
|
|
43
39
|
"default": "./dist/client.js"
|
|
44
40
|
},
|
|
41
|
+
"./const": {
|
|
42
|
+
"types": "./dist/const.d.ts",
|
|
43
|
+
"default": "./dist/const.js"
|
|
44
|
+
},
|
|
45
|
+
"./server": {
|
|
46
|
+
"types": "./dist/server.d.ts",
|
|
47
|
+
"default": "./dist/server.js"
|
|
48
|
+
},
|
|
45
49
|
"./src": {
|
|
46
50
|
"types": "./src/index.ts",
|
|
47
51
|
"default": "./src/index.ts"
|
|
@@ -53,10 +57,12 @@
|
|
|
53
57
|
},
|
|
54
58
|
"files": [
|
|
55
59
|
"dist",
|
|
60
|
+
"mermaid",
|
|
56
61
|
"README.md"
|
|
57
62
|
],
|
|
58
63
|
"scripts": {
|
|
59
|
-
"build": "tsdown --config ../../tsdown.config.ts"
|
|
64
|
+
"build": "tsdown --config ../../tsdown.config.ts",
|
|
65
|
+
"mmd": "for f in concept stack grant; do mmdc -i media/$f.mmd -o media/$f.svg; done"
|
|
60
66
|
},
|
|
61
67
|
"devDependencies": {
|
|
62
68
|
"@auth/core": "0.34.3",
|