caspian-utils 0.1.5 → 0.1.7

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/dist/docs/auth.md CHANGED
@@ -20,14 +20,14 @@ Treat `casp.auth` as the default authentication layer in Caspian app code. Do no
20
20
 
21
21
  ## Overview
22
22
 
23
- Caspian authentication has two main layers:
24
-
25
- - app-level policy, controlled by `src/lib/auth/auth_config.py`
26
- - framework runtime behavior, implemented by `.venv/Lib/site-packages/casp/auth.py`
23
+ Caspian authentication has two main layers:
24
+
25
+ - app-level policy, controlled by `src/lib/auth/auth_config.py`
26
+ - framework runtime behavior, implemented by `.venv/Lib/site-packages/casp/auth.py`
27
27
 
28
28
  The main public API includes:
29
29
 
30
- - `AuthSettings` for centralized app auth policy
30
+ - `AuthSettings` for centralized app auth policy
31
31
  - `configure_auth(...)` and `get_auth_settings()` for app startup and reads
32
32
  - the global `auth` object for session lifecycle work
33
33
  - `require_auth`, `require_role`, and `guest_only` for page-level protection
@@ -49,7 +49,7 @@ from casp.auth import (
49
49
 
50
50
  ## Default Auth Rule
51
51
 
52
- - Define app-wide auth behavior in `src/lib/auth/auth_config.py` through `build_auth_settings()` and apply it once at startup with `configure_auth(...)`.
52
+ - Define app-wide auth behavior in `src/lib/auth/auth_config.py` through `build_auth_settings()` and apply it once at startup with `configure_auth(...)`.
53
53
  - Use `auth.sign_in(...)` and `auth.sign_out(...)` instead of setting or clearing session keys directly.
54
54
  - Prefer `pp.rpc(...)` plus `@rpc(require_auth=True)` for signout buttons or menus rendered in pages or components. Use a dedicated signout route only when you need a plain HTML form POST or a no-JavaScript fallback.
55
55
  - Use `@require_auth`, `@require_role`, and `@guest_only` for page access rules.
@@ -61,15 +61,15 @@ from casp.auth import (
61
61
 
62
62
  ## Framework Internals Note
63
63
 
64
- - The centralized app auth policy controller is `src/lib/auth/auth_config.py`.
65
- - The installed framework implementation lives in `.venv/Lib/site-packages/casp/auth.py`.
66
- - Treat `auth_config.py` as project code and `casp/auth.py` as framework runtime code. Do not edit `casp/auth.py` to control app route privacy, redirects, or RBAC.
64
+ - The centralized app auth policy controller is `src/lib/auth/auth_config.py`.
65
+ - The installed framework implementation lives in `.venv/Lib/site-packages/casp/auth.py`.
66
+ - Treat `auth_config.py` as project code and `casp/auth.py` as framework runtime code. Do not edit `casp/auth.py` to control app route privacy, redirects, or RBAC.
67
67
  - If upstream docs and the installed implementation disagree, prefer the installed implementation for local project guidance.
68
68
  - Use [core-runtime-map.md](./core-runtime-map.md) when an auth task crosses `main.py` bootstrap behavior such as development cookie scoping or middleware ownership.
69
69
 
70
70
  ## Centralized Auth Settings
71
71
 
72
- Keep application auth policy in `src/lib/auth/auth_config.py`. This file is the controller for route privacy, redirects, and RBAC.
72
+ Keep application auth policy in `src/lib/auth/auth_config.py`. This file is the controller for route privacy, redirects, and RBAC.
73
73
 
74
74
  Example:
75
75
 
@@ -80,12 +80,12 @@ from casp.auth import AuthSettings
80
80
 
81
81
  def build_auth_settings() -> AuthSettings:
82
82
  """
83
- Centralized app auth policy controller.
84
-
85
- Keep secrets (AUTH_SECRET, AUTH_COOKIE_NAME) in .env.
86
- Keep app-level session settings in .env (SESSION_LIFETIME_HOURS, etc).
87
- Decide route privacy, redirects, and RBAC here at app setup time instead of
88
- changing Caspian core runtime files.
83
+ Centralized app auth policy controller.
84
+
85
+ Keep secrets (AUTH_SECRET, AUTH_COOKIE_NAME) in .env.
86
+ Keep app-level session settings in .env (SESSION_LIFETIME_HOURS, etc).
87
+ Decide route privacy, redirects, and RBAC here at app setup time instead of
88
+ changing Caspian core runtime files.
89
89
  """
90
90
 
91
91
  return AuthSettings(
@@ -103,7 +103,7 @@ def build_auth_settings() -> AuthSettings:
103
103
  is_role_based=False,
104
104
  role_identifier="role",
105
105
 
106
- # RBAC policy is app-owned here; the runtime expects ROUTE/PATTERN -> [ROLES].
106
+ # RBAC policy is app-owned here; the runtime expects ROUTE/PATTERN -> [ROLES].
107
107
  role_based_routes={},
108
108
 
109
109
  # Redirects / prefixes
@@ -152,7 +152,7 @@ Make this decision at app setup time in `src/lib/auth/auth_config.py`.
152
152
  - In the current runtime, `auth_routes=["/signin", "/signup"]` stays public by default, and most apps do not need to change it unless the user explicitly asks for different auth routes.
153
153
  - In all-private mode, the default `public_routes=["/"]` keeps the home page public unless you change that list.
154
154
  - `token_auto_refresh=True` does not make routes private. It only enables sliding-session refresh when the request lifecycle calls `auth.refresh_session()`.
155
- - Do not modify Caspian core files for this decision. Keep the policy in `src/lib/auth/auth_config.py`.
155
+ - Do not modify Caspian core files for this decision. Keep the policy in `src/lib/auth/auth_config.py`.
156
156
  - If you customize `src/lib/auth/auth_config.py`, add it to `excludeFiles` in `caspian.config.json` so update commands do not overwrite your local auth policy.
157
157
 
158
158
  Example all-private setup with a few public exceptions:
@@ -720,6 +720,33 @@ In the installed implementation:
720
720
 
721
721
  The installed `casp.auth` file includes provider helpers for Google and GitHub OAuth.
722
722
 
723
+ In the app-owned starter bootstrap, both providers are already registered. Treat Google and GitHub sign-in as a shipped feature, not something to build from scratch. The current `main.py` calls:
724
+
725
+ ```python
726
+ Auth.set_providers(GithubProvider(), GoogleProvider())
727
+ ```
728
+
729
+ so `AuthMiddleware` already delegates the provider signin and callback paths through `auth.auth_providers(...)` on every request. To add a working social sign-in to a page, you usually need only two things:
730
+
731
+ - a link or button that navigates to the provider signin path, and
732
+ - the provider credentials in `.env`.
733
+
734
+ The signin and callback paths live under `api_auth_prefix` (default `/api/auth`):
735
+
736
+ - Google sign-in link target: `/api/auth/signin/google`
737
+ - GitHub sign-in link target: `/api/auth/signin/github`
738
+ - Google callback (provider-side redirect URI): `/api/auth/callback/google`
739
+ - GitHub callback (provider-side redirect URI): `/api/auth/callback/github`
740
+
741
+ Minimal sign-in button:
742
+
743
+ ```html
744
+ <a href="/api/auth/signin/google">Continue with Google</a>
745
+ <a href="/api/auth/signin/github">Continue with GitHub</a>
746
+ ```
747
+
748
+ Do not reinvent this flow. When a task asks for Google or GitHub login, do not add `authlib`, a hand-written `httpx` token-exchange, a parallel session writer, or custom `/callback` route handlers. The shipped providers plus `auth.auth_providers(...)` already perform the redirect, the code exchange, payload normalization, `auth.sign_in(...)`, and the post-login redirect to `default_signin_redirect`. Reuse them and keep credentials in `.env`.
749
+
723
750
  Available provider classes:
724
751
 
725
752
  - `GoogleProvider(client_id, client_secret, redirect_uri, max_age="30d")`
@@ -767,7 +794,7 @@ Behavior:
767
794
 
768
795
  Use this helper when custom form or fetch flows need access to the session CSRF token.
769
796
 
770
- ## Current Implementation Notes
797
+ ## Current Implementation Notes
771
798
 
772
799
  - The installed auth runtime is session-backed. It stores the auth payload and CSRF token in `request.session`.
773
800
  - Expiration uses timestamps and the current duration parser only accepts `s`, `m`, `h`, and `d` units.
@@ -810,7 +837,7 @@ If an AI agent is deciding how to handle auth in Caspian, apply these rules firs
810
837
  - Treat `public_routes` as the public exception list for all-private apps. In the current defaults, `/` stays public and `auth_routes=["/signin", "/signup"]` stays public too.
811
838
  - For protected grouped sections, follow the section layout pattern in [routing.md](./routing.md) and keep auth-specific route policy in `src/lib/auth/auth_config.py`.
812
839
  - Apply settings at startup in `main.py` with `configure_auth(build_auth_settings())`.
813
- - Register provider instances in `main.py` with `Auth.set_providers(...)` when Google or GitHub OAuth is enabled.
840
+ - Treat Google and GitHub OAuth as shipped: the starter `main.py` already registers both providers with `Auth.set_providers(GithubProvider(), GoogleProvider())`. For social sign-in, link to `/api/auth/signin/{google,github}` and set `.env` credentials; do not hand-roll OAuth, token exchange, or callback routes. Only call `Auth.set_providers(...)` yourself if you are changing which providers are registered.
814
841
  - Use `auth.sign_in(...)` and `auth.sign_out(...)` for session lifecycle changes.
815
842
  - Prefer `pp.rpc("signout")` plus `@rpc(require_auth=True)` for logout buttons or menus in pages and components.
816
843
  - Use a dedicated `/signout` route only for plain HTML form POST, no-JavaScript fallback, or other full-navigation edge cases.
@@ -21,13 +21,13 @@ related:
21
21
 
22
22
  This page explains how data fetching works in Caspian. Use route functions for initial page data and use RPC actions for browser-triggered reads, writes, streams, uploads, and normal CRUD work.
23
23
 
24
- Treat RPC as the default way for browser code to talk to Python in Caspian. For CRUD operations and any browser-initiated backend reads after first render, default to `@rpc()` on the server and `pp.rpc()` in PulsePoint code. Do not reach for ad hoc fetch calls to custom JSON endpoints, alternate transport layers, or older helper names unless the task explicitly requires that shape.
25
-
26
- WebSockets are the main exception for long-lived bidirectional live channels when `caspian.config.json` has `websocket: true`. If the task needs a native browser `WebSocket`, persistent broadcast channel, presence, live chat, or socket origin/auth handling, confirm that flag and read [websockets.md](./websockets.md) before adding endpoint or client code. Do not use WebSockets as a replacement for ordinary RPC form submits or CRUD work.
27
-
28
- When `caspian.config.json` has `prisma: true`, the Python side of those route and RPC actions must use the generated Prisma Python ORM from `src/lib/prisma/**` for database reads and writes. The browser asks Python through `pp.rpc(...)`; Python asks the database through Prisma. Do not replace either side with a custom browser fetch, direct database driver, hand-written SQL helper, JSON active store, or second app-owned data-fetch abstraction.
29
-
30
- Browser-triggered data work should still be PulsePoint-first at the event layer. Bind the initiating click, submit, input, upload, refresh, filter, or pagination control in authored HTML with `onclick`, `onsubmit`, `oninput`, `onchange`, or another native `on*` attribute handled by PulsePoint. For normal form submissions, read named controls with `Object.fromEntries(new FormData(event.currentTarget).entries())` in the submit handler and pass that object directly to `pp.rpc(...)`; the route's Python `@rpc()` action should validate, normalize, and decide what to persist. Do not set up first-party data actions by assigning ids and then wiring `querySelector(...)`, `addEventListener(...)`, manual `fetch(...)`, manual DOM repainting, or per-input `pp-ref` payload collection.
24
+ Treat RPC as the default way for browser code to talk to Python in Caspian. For CRUD operations and any browser-initiated backend reads after first render, default to `@rpc()` on the server and `pp.rpc()` in PulsePoint code. Do not reach for ad hoc fetch calls to custom JSON endpoints, alternate transport layers, or older helper names unless the task explicitly requires that shape.
25
+
26
+ WebSockets are the main exception for long-lived bidirectional live channels when `caspian.config.json` has `websocket: true`. If the task needs a native browser `WebSocket`, persistent broadcast channel, presence, live chat, or socket origin/auth handling, confirm that flag and read [websockets.md](./websockets.md) before adding endpoint or client code. Do not use WebSockets as a replacement for ordinary RPC form submits or CRUD work.
27
+
28
+ When `caspian.config.json` has `prisma: true`, the Python side of those route and RPC actions must use the generated Prisma Python ORM from `src/lib/prisma/**` for database reads and writes. The browser asks Python through `pp.rpc(...)`; Python asks the database through Prisma. Do not replace either side with a custom browser fetch, direct database driver, hand-written SQL helper, JSON active store, or second app-owned data-fetch abstraction.
29
+
30
+ Browser-triggered data work should still be PulsePoint-first at the event layer. Bind the initiating click, submit, input, upload, refresh, filter, or pagination control in authored HTML with `onclick`, `onsubmit`, `oninput`, `onchange`, or another native `on*` attribute handled by PulsePoint. For normal form submissions, read named controls with `Object.fromEntries(new FormData(event.currentTarget).entries())` in the submit handler and pass that object directly to `pp.rpc(...)`; the route's Python `@rpc()` action should validate, normalize, and decide what to persist. Do not set up first-party data actions by assigning ids and then wiring `querySelector(...)`, `addEventListener(...)`, manual `fetch(...)`, manual DOM repainting, or per-input `pp-ref` payload collection.
31
31
 
32
32
  MCP is a separate integration surface. Do not place app-owned FastMCP tools in route `index.py` files or treat `@rpc()` actions as a replacement for MCP tools. Use `mcp.md` and `src/lib/mcp/` only when `caspian.config.json` has `mcp: true`. If `mcp` is false, do not assume those files exist.
33
33
 
@@ -35,7 +35,7 @@ MCP is a separate integration surface. Do not place app-owned FastMCP tools in r
35
35
 
36
36
  Caspian has two main data-loading paths:
37
37
 
38
- - `page()` for initial-render data, plus `layout()` for shared props or metadata during the render
38
+ - `page()` for initial-render data, plus `layout()` for shared props or metadata during the render
39
39
  - `@rpc()` plus `pp.rpc()` for interactive fetches after the page is already loaded
40
40
 
41
41
  In practice, most pages use both:
@@ -50,13 +50,13 @@ When a page belongs to a grouped subtree such as a dashboard, account area, admi
50
50
 
51
51
  ## Default Data Rule
52
52
 
53
- - Use `page()` for route-level data required before HTML renders, and use `layout()` only for shared subtree props or metadata.
53
+ - Use `page()` for route-level data required before HTML renders, and use `layout()` only for shared subtree props or metadata.
54
54
  - When a route renders UI and also needs backend work, keep the HTML in the sibling `index.html`; `index.py` should prepare data and call `render_page(__file__, ...)`, not inline the route markup.
55
- - Use `@rpc()` on the server and `pp.rpc()` in PulsePoint code for all browser-triggered data work after first render, including CRUD operations and follow-up reads.
56
- - When Prisma is enabled, route all Python database I/O inside `page()`, `layout()` when truly shared, `@rpc()` actions, and reusable helpers through the generated Prisma Python ORM.
57
- - Trigger those browser actions through PulsePoint event attributes in the HTML, not through a separate DOM listener layer.
58
- - For simple form submits, prefer `onsubmit="{submitForm(event)}"` plus `Object.fromEntries(new FormData(event.currentTarget).entries())` over `pp-ref` fields and effect-managed submit listeners.
59
- - Keep custom REST or other endpoint patterns as explicit exceptions, not the baseline Caspian approach.
55
+ - Use `@rpc()` on the server and `pp.rpc()` in PulsePoint code for all browser-triggered data work after first render, including CRUD operations and follow-up reads.
56
+ - When Prisma is enabled, route all Python database I/O inside `page()`, `layout()` when truly shared, `@rpc()` actions, and reusable helpers through the generated Prisma Python ORM.
57
+ - Trigger those browser actions through PulsePoint event attributes in the HTML, not through a separate DOM listener layer.
58
+ - For simple form submits, prefer `onsubmit="{submitForm(event)}"` plus `Object.fromEntries(new FormData(event.currentTarget).entries())` over `pp-ref` fields and effect-managed submit listeners.
59
+ - Keep custom REST or other endpoint patterns as explicit exceptions, not the baseline Caspian approach.
60
60
 
61
61
  ## Initial Data In `index.py`
62
62
 
@@ -87,11 +87,11 @@ If a route's first-render HTML is public and stable enough to reuse across reque
87
87
  Notes:
88
88
 
89
89
  - Prefer `async def page()` when your database or API client is async-capable.
90
- - Put shared section-level props in `layout.py` when multiple child routes need the same payload. The current layout engine supports synchronous and async `layout()` results, but route-specific data should stay in `page()`.
90
+ - Put shared section-level props in `layout.py` when multiple child routes need the same payload. The current layout engine supports synchronous and async `layout()` results, but route-specific data should stay in `page()`.
91
91
  - Use a normal parent folder such as `dashboard/` when the section name should appear in the URL. Use a route-group folder such as `(reports)/` only when the shared wrapper should organize child routes without adding a URL segment.
92
92
  - Keep reusable database or API clients under `src/lib/`; keep route-specific orchestration in `src/app/`.
93
93
 
94
- If the data source is Prisma, confirm `caspian.config.json` has `prisma: true`, then see `database.md` for the project's schema, migration, and generation workflow. If the project includes an app-owned Python database layer under `src/lib/prisma/`, reuse it instead of creating another helper. In Prisma-enabled projects, direct driver access and custom fetch helpers are exceptions to avoid, not alternate defaults.
94
+ If the data source is Prisma, confirm `caspian.config.json` has `prisma: true`, then see `database.md` for the project's schema, migration, and generation workflow. If the project includes an app-owned Python database layer under `src/lib/prisma/`, reuse it instead of creating another helper. In Prisma-enabled projects, direct driver access and custom fetch helpers are exceptions to avoid, not alternate defaults.
95
95
 
96
96
  ## Interactive Data With RPC
97
97
 
@@ -113,18 +113,18 @@ from src.lib.prisma import prisma
113
113
  async def list_todos():
114
114
  return await prisma.todo.find_many()
115
115
 
116
- @rpc(require_auth=True, limits="20/minute")
117
- async def create_todo(title: str):
118
- title = title.strip()
119
- if not title:
120
- raise ValueError("Title is required.")
121
-
122
- return await prisma.todo.create(
123
- data={"title": title, "completed": False}
124
- )
116
+ @rpc(require_auth=True, limits="20/minute")
117
+ async def create_todo(title: str):
118
+ title = title.strip()
119
+ if not title:
120
+ raise ValueError("Title is required.")
121
+
122
+ return await prisma.todo.create(
123
+ data={"title": title, "completed": False}
124
+ )
125
125
  ```
126
126
 
127
- Call it from the client with `pp.rpc()`:
127
+ Call it from the client with `pp.rpc()`:
128
128
 
129
129
  ```html
130
130
  <script>
@@ -137,39 +137,39 @@ Call it from the client with `pp.rpc()`:
137
137
  const todo = await pp.rpc("create_todo", { title });
138
138
  console.log(todo);
139
139
  }
140
- </script>
141
- ```
142
-
143
- Do not call `fetch("/api/todos")` from the browser for normal Caspian CRUD when the route can expose an `@rpc()` action. Do not use `fetch()` or a database driver in Python to work around Prisma when `src/lib/prisma/**` already provides the generated ORM client.
144
-
145
- For form submissions, let the form event provide the submitted values. The inputs' `name` attributes define the RPC payload keys, and the Python action owns trimming, validation, coercion, and persistence decisions.
146
-
147
- ```html
148
- <form onsubmit="{createTodoFromForm(event)}" novalidate>
149
- <input name="title" required />
150
- <button type="submit" disabled="{isSaving}">Add</button>
151
-
152
- <script>
153
- const [isSaving, setIsSaving] = pp.state(false);
154
-
155
- async function createTodoFromForm(event) {
156
- event.preventDefault();
157
-
158
- const data = Object.fromEntries(new FormData(event.currentTarget).entries());
159
- if (isSaving) return;
160
-
161
- setIsSaving(true);
162
- try {
163
- await pp.rpc("create_todo", data, { abortPrevious: true });
164
- } finally {
165
- setIsSaving(false);
166
- }
167
- }
168
- </script>
169
- </form>
170
- ```
171
-
172
- Use RPC for:
140
+ </script>
141
+ ```
142
+
143
+ Do not call `fetch("/api/todos")` from the browser for normal Caspian CRUD when the route can expose an `@rpc()` action. Do not use `fetch()` or a database driver in Python to work around Prisma when `src/lib/prisma/**` already provides the generated ORM client.
144
+
145
+ For form submissions, let the form event provide the submitted values. The inputs' `name` attributes define the RPC payload keys, and the Python action owns trimming, validation, coercion, and persistence decisions.
146
+
147
+ ```html
148
+ <form onsubmit="{createTodoFromForm(event)}" novalidate>
149
+ <input name="title" required />
150
+ <button type="submit" disabled="{isSaving}">Add</button>
151
+
152
+ <script>
153
+ const [isSaving, setIsSaving] = pp.state(false);
154
+
155
+ async function createTodoFromForm(event) {
156
+ event.preventDefault();
157
+
158
+ const data = Object.fromEntries(new FormData(event.currentTarget).entries());
159
+ if (isSaving) return;
160
+
161
+ setIsSaving(true);
162
+ try {
163
+ await pp.rpc("create_todo", data, { abortPrevious: true });
164
+ } finally {
165
+ setIsSaving(false);
166
+ }
167
+ }
168
+ </script>
169
+ </form>
170
+ ```
171
+
172
+ Use RPC for:
173
173
 
174
174
  - CRUD reads and writes after the initial render
175
175
  - Button-triggered refreshes
@@ -186,8 +186,8 @@ Important:
186
186
 
187
187
  - `pp.rpc()` posts to the current route RPC bridge.
188
188
  - Older docs may refer to `pp.fetchFunction()`. In this repo's current runtime, the supported helper is `pp.rpc()`.
189
- - Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when debugging browser-side RPC options such as streaming, upload progress, abort behavior, redirects, or SPA interaction.
190
- - Use [websockets.md](./websockets.md) when `caspian.config.json` has `websocket: true` and the browser and server need a persistent bidirectional channel instead of request/response RPC or one-way SSE streaming.
189
+ - Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when debugging browser-side RPC options such as streaming, upload progress, abort behavior, redirects, or SPA interaction.
190
+ - Use [websockets.md](./websockets.md) when `caspian.config.json` has `websocket: true` and the browser and server need a persistent bidirectional channel instead of request/response RPC or one-way SSE streaming.
191
191
 
192
192
  ## Streaming Responses
193
193
 
@@ -227,6 +227,43 @@ Client example:
227
227
 
228
228
  Use streaming when the user should see partial progress instead of waiting for a single final payload.
229
229
 
230
+ This is the shipped, default mechanism for AI/LLM/chat token streaming in Caspian. When a task asks for a chat app, an assistant reply, or any "stream the model's tokens as they arrive" experience, use this RPC streaming path. Do not reinvent one-way streaming with raw `fetch` + `ReadableStream`, a browser `EventSource`, or a WebSocket. WebSockets are only for genuinely bidirectional channels (see [websockets.md](./websockets.md)); a one-way token stream from server to browser belongs in a generator `@rpc()` consumed by `pp.rpc(..., { onStream })`.
231
+
232
+ Bridging a Python LLM/SDK stream is the common case: `async for` over the provider's streaming response inside the `@rpc()` action and `yield` each chunk. The runtime turns the generator into an SSE response automatically.
233
+
234
+ ```python
235
+ from casp.rpc import rpc
236
+ from anthropic import AsyncAnthropic
237
+
238
+ client = AsyncAnthropic()
239
+
240
+ @rpc(require_auth=True, limits="20/minute")
241
+ async def chat(message: str):
242
+ async with client.messages.stream(
243
+ model="claude-opus-4-8",
244
+ max_tokens=1024,
245
+ messages=[{"role": "user", "content": message}],
246
+ ) as stream:
247
+ async for text in stream.text_stream:
248
+ yield text
249
+ ```
250
+
251
+ Client side, append each chunk to reactive state so the UI grows token by token:
252
+
253
+ ```html
254
+ <script>
255
+ const [answer, setAnswer] = pp.state("");
256
+
257
+ async function send(message) {
258
+ setAnswer("");
259
+ await pp.rpc("chat", { message }, {
260
+ onStream: (chunk) => setAnswer((current) => current + chunk),
261
+ onStreamComplete: () => console.log("done"),
262
+ });
263
+ }
264
+ </script>
265
+ ```
266
+
230
267
  In the current runtime, generator and async-generator RPC results are wrapped with `SSE(...)` inside `casp.rpc.py`, and route-level generator results are also wrapped by `main.py` before the response is returned.
231
268
 
232
269
  ## File Uploads
@@ -334,7 +371,7 @@ For initial route rendering with `render_page(...)`, prefer passing plain templa
334
371
 
335
372
  ## Recommended Decision Rule
336
373
 
337
- Use `page()` when route-specific data is part of the page render. Use `layout()` for shared subtree props or metadata. Use `@rpc()` plus `pp.rpc()` when the browser needs to ask the server for more data after the page is already visible.
374
+ Use `page()` when route-specific data is part of the page render. Use `layout()` for shared subtree props or metadata. Use `@rpc()` plus `pp.rpc()` when the browser needs to ask the server for more data after the page is already visible.
338
375
 
339
376
  A common pattern is:
340
377
 
@@ -366,8 +403,8 @@ If the first-render HTML is expensive to produce and safe to share between visit
366
403
  If an AI agent is choosing how to load data in Caspian, apply these rules first.
367
404
 
368
405
  - Put first-render data loading in `src/app/**/index.py`.
369
- - Put shared section props in `layout.py` only when multiple child routes need the same shared data.
370
- - Keep route-specific async I/O in `page()` or `@rpc()` so `layout.py` stays focused on shared subtree props or metadata.
406
+ - Put shared section props in `layout.py` only when multiple child routes need the same shared data.
407
+ - Keep route-specific async I/O in `page()` or `@rpc()` so `layout.py` stays focused on shared subtree props or metadata.
371
408
  - For grouped subtrees, follow the section layout pattern in [routing.md](./routing.md) before deciding where `page()` data and `@rpc()` actions belong.
372
409
  - Treat RPC as the default read and write layer between PulsePoint code and Python route logic, especially for CRUD and interactive backend reads.
373
410
  - Use `@rpc()` for backend functions that should be callable from the browser.
@@ -377,6 +414,7 @@ If an AI agent is choosing how to load data in Caspian, apply these rules first.
377
414
  - Use [cache.md](./cache.md) when a route's initial HTML should be reused across requests and invalidated after writes.
378
415
  - Use [state.md](./state.md) when an RPC mutation needs transient request-scoped success or error state outside the direct response payload.
379
416
  - Use `onStream` for streamed responses and `onUploadProgress` for upload-aware client calls.
417
+ - For AI/LLM/chat token streaming, default to a generator `@rpc()` that `yield`s chunks (bridge a Python SDK stream with `async for ... yield`) consumed by `pp.rpc(..., { onStream })`. Do not reinvent one-way streaming with raw `fetch`/`ReadableStream`, `EventSource`, or WebSockets.
380
418
  - Add `require_auth`, `allowed_roles`, and `limits` to sensitive RPC actions.
381
419
  - Keep reusable database clients and service wrappers in `src/lib/`.
382
420
  - Use `.venv/Lib/site-packages/casp/rpc.py` only when the task is about Caspian core RPC internals or framework debugging.
@@ -346,11 +346,12 @@ Important:
346
346
  - The script lookup walks the current root and skips nested `pp-component` boundaries, so a parent does not consume a child component's script.
347
347
  - If multiple matching runtime scripts exist in the same root, the first matching owned script wins. Generate one script per root.
348
348
  - Authored route, layout, and component templates still need one top-level parent node so Caspian can inject the component boundary correctly after component expansion.
349
- - A scriptless component root still mounts and can receive props, refs, events, and nested children, but it has no local runtime scope beyond its props.
349
+ - A scriptless component root still mounts and can receive props, refs, events, and nested children, but it does not create local bindings from those props on its own.
350
350
  - Component scripts are plain JavaScript executed with `new Function(...)`. Do not use `import`, `export`, or top-level `await` inside them.
351
351
  - The runtime auto-returns supported top-level bindings from the script. Do not rely on manual `return { ... }` objects.
352
- - `pp.props` contains the current prop bag for the component.
353
- - `pp.props.children` contains the root's initial inner HTML before the owned script is removed from the render template.
352
+ - `pp.props` contains the current prop bag for the component.
353
+ - `pp.props.children` contains the root's initial inner HTML before the owned script is removed from the render template.
354
+ - Props are not auto-injected as standalone top-level template or handler variables. Read them through `pp.props` or explicitly destructure them in the component script.
354
355
 
355
356
  Bindings exported to the template:
356
357
 
@@ -569,12 +570,13 @@ When a child component needs the same token object, pass it from the provider sc
569
570
 
570
571
  - Child component props are derived from DOM attributes.
571
572
  - Attribute names are converted from kebab-case to camelCase for the prop bag.
572
- - Native `on*` attributes and `pp-component` are not included in props.
573
- - Empty attributes become boolean `true` props.
574
- - Pure prop expressions such as `title="{pageTitle}"` are evaluated in the parent scope.
575
- - Mixed strings such as `class="card {isActive ? 'active' : ''}"` are interpolated in the parent scope.
576
- - Non-expression attribute values are passed through as strings.
577
- - `children` is injected into props using the root's initial inner HTML.
573
+ - Native `on*` attributes and `pp-component` are not included in props.
574
+ - Empty attributes become boolean `true` props.
575
+ - Pure prop expressions such as `title="{pageTitle}"` are evaluated in the parent scope.
576
+ - Mixed strings such as `class="card {isActive ? 'active' : ''}"` are interpolated in the parent scope.
577
+ - Non-expression attribute values are passed through as strings.
578
+ - `children` is injected into props using the root's initial inner HTML.
579
+ - Inside the child component, those values should be accessed through `pp.props` or a top-level destructure such as `const { title } = pp.props`; they are not implicitly available as bare identifiers.
578
580
 
579
581
  Nested components:
580
582
 
@@ -751,7 +753,7 @@ Upload with progress pattern:
751
753
  </section>
752
754
  ```
753
755
 
754
- Streaming pattern for server generators that return `text/event-stream`:
756
+ Streaming pattern for server generators that return `text/event-stream`. This is the default path for AI/LLM/chat token streaming: a generator `@rpc()` that `yield`s tokens (often by `async for`-ing over a Python SDK stream) consumed by `pp.rpc(..., { onStream })` that appends each chunk to reactive state. Do not reinvent one-way streaming with raw `fetch`/`ReadableStream`, `EventSource`, or a WebSocket; see [fetch-data.md](./fetch-data.md) for the Python side.
755
757
 
756
758
  ```html
757
759
  <section>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {