tep 0.11.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>tep chatbot — compare backends</title>
|
|
6
|
+
<link rel="stylesheet" href="/style.css">
|
|
7
|
+
</head>
|
|
8
|
+
<body class="app">
|
|
9
|
+
<aside class="sidebar">
|
|
10
|
+
<div class="sidebar-head">
|
|
11
|
+
<strong>tep chatbot</strong>
|
|
12
|
+
<a href="/" class="back-link">← Back</a>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="sidebar-section">Compare mode</div>
|
|
15
|
+
<p class="sidebar-blurb">
|
|
16
|
+
One prompt fanned out to N backends in parallel via
|
|
17
|
+
<code>Tep::Parallel</code> + fork. Each card below is one
|
|
18
|
+
backend's reply.
|
|
19
|
+
</p>
|
|
20
|
+
<div class="sidebar-foot">
|
|
21
|
+
<form method="post" action="/logout" class="logout-form">
|
|
22
|
+
<button type="submit">Log out</button>
|
|
23
|
+
</form>
|
|
24
|
+
</div>
|
|
25
|
+
</aside>
|
|
26
|
+
|
|
27
|
+
<main id="compare">
|
|
28
|
+
<header class="topbar">
|
|
29
|
+
<span>compare backends</span>
|
|
30
|
+
</header>
|
|
31
|
+
<form id="prompt-form" class="compare-prompt">
|
|
32
|
+
<textarea id="prompt-input" rows="3" placeholder="Ask the same thing of every backend…" autofocus></textarea>
|
|
33
|
+
<button type="submit" id="prompt-btn">Send to all</button>
|
|
34
|
+
</form>
|
|
35
|
+
<p id="compare-status" class="status"></p>
|
|
36
|
+
<div id="compare-grid" class="compare-grid"></div>
|
|
37
|
+
</main>
|
|
38
|
+
|
|
39
|
+
<script id="bootbackends" type="application/json"><%= @backends_json %></script>
|
|
40
|
+
<script src="/markdown.js"></script>
|
|
41
|
+
<script src="/compare.js"></script>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>tep chatbot</title>
|
|
6
|
+
<link rel="stylesheet" href="/style.css">
|
|
7
|
+
</head>
|
|
8
|
+
<body class="app">
|
|
9
|
+
<aside class="sidebar">
|
|
10
|
+
<div class="sidebar-head">
|
|
11
|
+
<strong>tep chatbot</strong>
|
|
12
|
+
<button id="new-conv-btn" type="button">+ New</button>
|
|
13
|
+
</div>
|
|
14
|
+
<ol id="conv-list" class="conv-list"></ol>
|
|
15
|
+
<div class="sidebar-section"><a href="/compare" class="back-link">Compare backends →</a></div>
|
|
16
|
+
<div class="sidebar-foot">
|
|
17
|
+
<form method="post" action="/logout" class="logout-form">
|
|
18
|
+
<button type="submit">Log out</button>
|
|
19
|
+
</form>
|
|
20
|
+
</div>
|
|
21
|
+
</aside>
|
|
22
|
+
|
|
23
|
+
<main id="chat" data-conv-id="<%= @conv_id %>">
|
|
24
|
+
<header class="topbar">
|
|
25
|
+
<span class="model"><%= @model %></span>
|
|
26
|
+
<span class="backend"><%= @backend %></span>
|
|
27
|
+
</header>
|
|
28
|
+
<ol id="messages" class="messages"></ol>
|
|
29
|
+
<form id="composer" class="composer">
|
|
30
|
+
<textarea id="composer-input" rows="3" placeholder="Send a message…" autofocus></textarea>
|
|
31
|
+
<button type="submit" id="send-btn">Send</button>
|
|
32
|
+
</form>
|
|
33
|
+
<p id="status" class="status"></p>
|
|
34
|
+
</main>
|
|
35
|
+
|
|
36
|
+
<!-- Boot data: current conversation messages + sidebar conversations. -->
|
|
37
|
+
<script id="bootmsgs" type="application/json"><%= @messages_json %></script>
|
|
38
|
+
<script id="bootconvs" type="application/json"><%= @conversations_json %></script>
|
|
39
|
+
<script src="/markdown.js"></script>
|
|
40
|
+
<script src="/chat.js"></script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>tep chatbot — login</title>
|
|
6
|
+
<link rel="stylesheet" href="/style.css">
|
|
7
|
+
</head>
|
|
8
|
+
<body class="auth-page">
|
|
9
|
+
<main class="auth-card">
|
|
10
|
+
<h1>Log in</h1>
|
|
11
|
+
<% if @error %>
|
|
12
|
+
<p class="error"><%= @error %></p>
|
|
13
|
+
<% end %>
|
|
14
|
+
<form method="post" action="/login">
|
|
15
|
+
<label>Password
|
|
16
|
+
<input type="password" name="password" autofocus required>
|
|
17
|
+
</label>
|
|
18
|
+
<button type="submit">Log in</button>
|
|
19
|
+
</form>
|
|
20
|
+
</main>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>tep chatbot — first-boot setup</title>
|
|
6
|
+
<link rel="stylesheet" href="/style.css">
|
|
7
|
+
</head>
|
|
8
|
+
<body class="auth-page">
|
|
9
|
+
<main class="auth-card">
|
|
10
|
+
<h1>Set a password</h1>
|
|
11
|
+
<p class="hint">This is the first boot. Pick a password to protect this chatbot — you'll use it to log in on subsequent visits.</p>
|
|
12
|
+
<% if @error %>
|
|
13
|
+
<p class="error"><%= @error %></p>
|
|
14
|
+
<% end %>
|
|
15
|
+
<form method="post" action="/setup">
|
|
16
|
+
<label>Password (6+ characters)
|
|
17
|
+
<input type="password" name="password" autofocus required minlength="6">
|
|
18
|
+
</label>
|
|
19
|
+
<button type="submit">Set password</button>
|
|
20
|
+
</form>
|
|
21
|
+
</main>
|
|
22
|
+
</body>
|
|
23
|
+
</html>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# counter -- the smallest Tep::LiveView demo
|
|
2
|
+
|
|
3
|
+
A single shared integer counter, server-side. Open in two browsers,
|
|
4
|
+
click `+` in one, watch the other update.
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
┌──────────────────────────────┐
|
|
8
|
+
│ SHARED COUNTER │
|
|
9
|
+
│ │
|
|
10
|
+
│ 7 │
|
|
11
|
+
│ │
|
|
12
|
+
│ [ - ] [ + ] │
|
|
13
|
+
│ reset │
|
|
14
|
+
│ │
|
|
15
|
+
│ open this page in another │
|
|
16
|
+
│ tab to see live updates. │
|
|
17
|
+
└──────────────────────────────┘
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Run
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
bin/tep build examples/counter/app.rb -o /tmp/counter
|
|
24
|
+
/tmp/counter -p 4567
|
|
25
|
+
# open http://127.0.0.1:4567/counter in two browsers
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## What it shows
|
|
29
|
+
|
|
30
|
+
This is the smallest possible app that exercises three pieces of
|
|
31
|
+
the LiveView surface at once:
|
|
32
|
+
|
|
33
|
+
| Piece | What it does here |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `Tep.live "/counter", CounterView` | One DSL call lowers to GET `/counter` (initial render + bootstrap JS) **and** WS `/counter/ws` (event dispatch + re-render). No manual `websocket` block. |
|
|
36
|
+
| `CounterView#topic` | Returns a stable string. Every WS connection that opens against this view subscribes to that topic automatically; `broadcast_render` fans out to all of them. |
|
|
37
|
+
| `broadcast_render` | After mutating the shared `COUNTER`, every subscriber sees the new HTML in <100ms via a single WS TEXT frame. The bootstrap JS in `Tep::LiveView.render_page` does `outerHTML = e.data` on `#tep-live-root`. |
|
|
38
|
+
|
|
39
|
+
## Code
|
|
40
|
+
|
|
41
|
+
~30 lines of Ruby for the view + handler; ~20 lines of inline CSS.
|
|
42
|
+
No JS to write -- click + re-render comes from the bootstrap
|
|
43
|
+
shell that `Tep.live` wires automatically. Clicks on any element
|
|
44
|
+
with `data-event="..."` send `{"event": <name>, "payload": ""}`
|
|
45
|
+
over the WS; the server's `handle_event` mutates state + calls
|
|
46
|
+
`broadcast_render`; every subscriber re-renders.
|
|
47
|
+
|
|
48
|
+
## Shared state
|
|
49
|
+
|
|
50
|
+
The counter lives in a module-level `COUNTER = [0]` array
|
|
51
|
+
(single-element typed slot, because spinel doesn't track module-
|
|
52
|
+
level `@@cvar` writes reliably across method calls). Per-worker
|
|
53
|
+
scope: a multi-worker deployment would need
|
|
54
|
+
`Tep::Broadcast.enable_pg_backend` to route NOTIFYs through PG so
|
|
55
|
+
all workers see the same mutations. Left out here for demo
|
|
56
|
+
simplicity.
|
|
57
|
+
|
|
58
|
+
## What this demo does NOT show
|
|
59
|
+
|
|
60
|
+
- **Per-user state.** Every browser sees the same number. For
|
|
61
|
+
per-user LiveView state, give the view per-instance ivars +
|
|
62
|
+
seed them in `mount(req)` from `req.identity` or `req.params`.
|
|
63
|
+
- **Authentication.** No `Tep.session_secret` / `Tep::Auth.install!`
|
|
64
|
+
here. The presence-aware four-battery surface lives in
|
|
65
|
+
[`examples/agentic_chat`](../agentic_chat).
|
|
66
|
+
- **History / persistence.** The counter resets to 0 on every
|
|
67
|
+
server restart. For persistent state, mutate a `Tep::SQLite`
|
|
68
|
+
table from `handle_event`.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Counter -- minimal Tep::LiveView demo using Tep.live auto-wiring.
|
|
2
|
+
#
|
|
3
|
+
# A single shared integer counter. Every open browser is subscribed
|
|
4
|
+
# to the same topic; clicking + / - / reset mutates the shared
|
|
5
|
+
# state and broadcasts the re-rendered HTML to every subscriber.
|
|
6
|
+
# All connections see the new value in <100ms with no polling and
|
|
7
|
+
# no full-page reload.
|
|
8
|
+
#
|
|
9
|
+
# Run:
|
|
10
|
+
# bin/tep build examples/counter/app.rb -o /tmp/counter
|
|
11
|
+
# /tmp/counter -p 4567
|
|
12
|
+
# Open http://127.0.0.1:4567/counter in two browsers and click +.
|
|
13
|
+
require 'sinatra'
|
|
14
|
+
|
|
15
|
+
set :scheduler, :scheduled
|
|
16
|
+
|
|
17
|
+
# Single-element typed array as a shared int slot. (Spinel doesn't
|
|
18
|
+
# track module-level `@@cvar` writes reliably across method calls;
|
|
19
|
+
# an Array[Integer] gives us a typed shared slot that survives
|
|
20
|
+
# request boundaries.)
|
|
21
|
+
COUNTER = [0]
|
|
22
|
+
|
|
23
|
+
class CounterView < Tep::LiveView
|
|
24
|
+
# Topic binds every connected viewer to the same broadcast stream.
|
|
25
|
+
# On every event, broadcast_render fans the updated HTML out to
|
|
26
|
+
# all subscribers (each WS on this topic).
|
|
27
|
+
def topic
|
|
28
|
+
"counter:shared"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render
|
|
32
|
+
"<div id='tep-live-root' class='counter'>" +
|
|
33
|
+
"<h1>shared counter</h1>" +
|
|
34
|
+
"<p class='value'>" + COUNTER[0].to_s + "</p>" +
|
|
35
|
+
"<div class='controls'>" +
|
|
36
|
+
"<button data-event='dec'>−</button>" +
|
|
37
|
+
"<button data-event='inc'>+</button>" +
|
|
38
|
+
"</div>" +
|
|
39
|
+
"<button class='reset' data-event='reset'>reset</button>" +
|
|
40
|
+
"<p class='hint'>open this page in another tab to see live updates.</p>" +
|
|
41
|
+
"</div>" +
|
|
42
|
+
"<style>" + counter_css + "</style>"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def handle_event(event, payload, req)
|
|
46
|
+
if event == "inc"
|
|
47
|
+
COUNTER[0] = COUNTER[0] + 1
|
|
48
|
+
broadcast_render
|
|
49
|
+
elsif event == "dec"
|
|
50
|
+
COUNTER[0] = COUNTER[0] - 1
|
|
51
|
+
broadcast_render
|
|
52
|
+
elsif event == "reset"
|
|
53
|
+
COUNTER[0] = 0
|
|
54
|
+
broadcast_render
|
|
55
|
+
end
|
|
56
|
+
0
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def counter_css
|
|
61
|
+
"body{margin:0;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;" +
|
|
62
|
+
"background:#f6f7f9;color:#1a1a1a;" +
|
|
63
|
+
"display:flex;justify-content:center;align-items:center;min-height:100vh}" +
|
|
64
|
+
".counter{background:#fff;padding:2rem 3rem;border-radius:8px;" +
|
|
65
|
+
"box-shadow:0 1px 4px rgba(0,0,0,.06);text-align:center;min-width:300px}" +
|
|
66
|
+
".counter h1{margin:0 0 1rem;font-size:.85rem;text-transform:uppercase;" +
|
|
67
|
+
"letter-spacing:.1em;color:#666;font-weight:600}" +
|
|
68
|
+
".counter .value{margin:0 0 1.5rem;font-size:4rem;font-weight:700;" +
|
|
69
|
+
"font-variant-numeric:tabular-nums;color:#1a1a1a}" +
|
|
70
|
+
".counter .controls{display:flex;gap:.5rem;justify-content:center;margin-bottom:1rem}" +
|
|
71
|
+
".counter button{padding:.6rem 1.4rem;border:1px solid #d0d3d8;background:#fafbfc;" +
|
|
72
|
+
"color:#1a1a1a;border-radius:4px;font:inherit;font-size:1.2rem;cursor:pointer}" +
|
|
73
|
+
".counter button:hover{background:#1a1a1a;color:#fff;border-color:#1a1a1a}" +
|
|
74
|
+
".counter button.reset{font-size:.8rem;padding:.3rem .8rem;color:#888;background:transparent}" +
|
|
75
|
+
".counter button.reset:hover{background:#f0f1f3;color:#1a1a1a;border-color:#d0d3d8}" +
|
|
76
|
+
".counter .hint{margin:1rem 0 0;font-size:.75rem;color:#888;font-style:italic}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
Tep.live "/counter", CounterView
|
|
80
|
+
|
|
81
|
+
get '/' do
|
|
82
|
+
res.set_status(302)
|
|
83
|
+
res.headers["Location"] = "/counter"
|
|
84
|
+
""
|
|
85
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file documents the agent-facing surface of this app for any
|
|
4
|
+
LLM / agent reading it. The convention: every tep app exposing
|
|
5
|
+
MCP ships an `AGENTS.md` at its repo root so a Claude Code (or
|
|
6
|
+
OpenCode / Gravity / etc.) session can read it once and know
|
|
7
|
+
how to drive the app safely.
|
|
8
|
+
|
|
9
|
+
## What this app does
|
|
10
|
+
|
|
11
|
+
A mock training-run manager. Agents start named experiments with
|
|
12
|
+
hyperparameters, advance them one epoch at a time, list current
|
|
13
|
+
state, and cancel runs.
|
|
14
|
+
|
|
15
|
+
The training is simulated for demo purposes; the agent-facing
|
|
16
|
+
surface is the production shape.
|
|
17
|
+
|
|
18
|
+
## How to discover
|
|
19
|
+
|
|
20
|
+
| URL | Purpose |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `/mcp` (POST, JSON-RPC 2.0) | Primary surface. Speak MCP here. |
|
|
23
|
+
| `/llms.txt` (GET) | Plain-text catalog of tools + resources. |
|
|
24
|
+
| `/openapi.json` (GET) | OpenAPI 3.0.3 of the HTTP-direct surface. |
|
|
25
|
+
|
|
26
|
+
Inside `/mcp`: `initialize` returns server info + capabilities;
|
|
27
|
+
`tools/list` enumerates tools; `resources/list` enumerates
|
|
28
|
+
resources; `tools/call` and `resources/read` invoke them.
|
|
29
|
+
|
|
30
|
+
## Tool catalog
|
|
31
|
+
|
|
32
|
+
| Tool | Caps required | Effect |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `start_experiment(name, learning_rate, epochs)` | `:run_experiments` | Enqueue + auto-start a run. Returns the new id. |
|
|
35
|
+
| `step_experiment(id)` | (none) | Advance one epoch. Idempotent on `done` / `cancelled`. |
|
|
36
|
+
| `list_experiments()` | (none) | Snapshot of every experiment as `id=N name=... status=... epoch=K/N loss=L1,L2,...`. |
|
|
37
|
+
| `cancel_experiment(id)` | `:run_experiments` | Mark a run as `cancelled`. Reversible: re-call `start_experiment` to start a new run with the same name. |
|
|
38
|
+
|
|
39
|
+
## Resource catalog
|
|
40
|
+
|
|
41
|
+
| Resource URI | mimeType | Effect |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `experiments/all` | `text/plain` | Snapshot of every experiment. Same format as `list_experiments`. |
|
|
44
|
+
| `experiments/active` | `text/plain` | Only runs with `status=running`. |
|
|
45
|
+
|
|
46
|
+
Resources are read-only fetches. Use them for periodic state
|
|
47
|
+
polling between tool calls.
|
|
48
|
+
|
|
49
|
+
## Invariants the app maintains
|
|
50
|
+
|
|
51
|
+
- Experiment ids are monotonically increasing (1, 2, 3, ...).
|
|
52
|
+
- Status transitions: `queued -> running -> {done | cancelled}`.
|
|
53
|
+
Never goes backward.
|
|
54
|
+
- `step_experiment` on `done` or `cancelled` is a no-op.
|
|
55
|
+
- `cancel_experiment` on `done` is allowed but flips status back
|
|
56
|
+
to `cancelled` -- avoid if you want to preserve "completed" runs.
|
|
57
|
+
- Loss series is append-only; no in-place edits.
|
|
58
|
+
|
|
59
|
+
## How to drive efficiently
|
|
60
|
+
|
|
61
|
+
- **Start a batch**, then `step` each run round-robin until all
|
|
62
|
+
complete. Listing in a loop polls cheaply (`list_experiments`
|
|
63
|
+
is O(n)).
|
|
64
|
+
- **Compare runs** by reading `experiments/all` and parsing the
|
|
65
|
+
loss arrays per id. The format is stable; agents can string-
|
|
66
|
+
split safely.
|
|
67
|
+
- **Cancel early** when an experiment's loss curve is clearly
|
|
68
|
+
worse than alternatives. Don't wait for it to finish.
|
|
69
|
+
|
|
70
|
+
## Authorization
|
|
71
|
+
|
|
72
|
+
For the demo this app accepts an `X-Demo-Cap-Run: 1` header as a
|
|
73
|
+
stand-in for the capability. A real deployment uses
|
|
74
|
+
`Tep::AuthOAuth2` to mint a JWT delegating the agent
|
|
75
|
+
`run_experiments` capability on behalf of a human user; the
|
|
76
|
+
agent passes that JWT as `Authorization: Bearer <token>` on
|
|
77
|
+
every `/mcp` POST. The tool body sees `req.identity` with
|
|
78
|
+
`acting_via` set to the delegation, exactly as the framework
|
|
79
|
+
documents.
|
|
80
|
+
|
|
81
|
+
## Things you should NOT do
|
|
82
|
+
|
|
83
|
+
- **Don't restart runs to force a different seed.** This
|
|
84
|
+
particular app is deterministic; cancel + start is the same as
|
|
85
|
+
step + step.
|
|
86
|
+
- **Don't start more than ~10 concurrent runs.** State is in-
|
|
87
|
+
memory; the app has no backpressure or queuing.
|
|
88
|
+
- **Don't assume `loss` values are real ML output.** They're
|
|
89
|
+
synthetic for the demo. Use the shape of the API to drive
|
|
90
|
+
reasoning about agent loops; don't extrapolate to actual
|
|
91
|
+
hyperparameter search conclusions.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# experiments -- the MCP battery demo
|
|
2
|
+
|
|
3
|
+
A mock training-run manager driven by an MCP client. The full
|
|
4
|
+
agent-as-driver loop in ~200 lines of Ruby:
|
|
5
|
+
|
|
6
|
+
- 4 `mcp_tool`s (start / step / list / cancel)
|
|
7
|
+
- 2 `mcp_resource`s (all / active)
|
|
8
|
+
- Capability gating on the mutating tools (`:run_experiments`)
|
|
9
|
+
- Auto-published catalog at `/llms.txt`, `/openapi.json`, `/mcp`
|
|
10
|
+
|
|
11
|
+
The training is **simulated** -- no actual ML. State lives in
|
|
12
|
+
module-level arrays. A real runner would persist to SQLite via
|
|
13
|
+
the same tool/resource API; nothing in the agent-facing surface
|
|
14
|
+
would change.
|
|
15
|
+
|
|
16
|
+
## Run
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bin/tep build examples/experiments/app.rb -o /tmp/experiments
|
|
20
|
+
/tmp/experiments -p 4567
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Open `http://127.0.0.1:4567/` in a browser for the landing
|
|
24
|
+
page (lists the tool + resource catalog with quick-start curl
|
|
25
|
+
recipes).
|
|
26
|
+
|
|
27
|
+
## Drive it from Claude Code (or any MCP client)
|
|
28
|
+
|
|
29
|
+
Point your MCP client at `http://127.0.0.1:4567/mcp`. Three
|
|
30
|
+
methods cover everything:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
initialize -> handshake
|
|
34
|
+
tools/list -> discover the 4 tools
|
|
35
|
+
tools/call -> run one (e.g. start_experiment + step_experiment)
|
|
36
|
+
resources/list -> discover the 2 resources
|
|
37
|
+
resources/read -> read a resource by URI
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The client doesn't need to know any HTTP details beyond the
|
|
41
|
+
`/mcp` URL -- everything else (tool schemas, capability checks,
|
|
42
|
+
error reporting) flows through JSON-RPC.
|
|
43
|
+
|
|
44
|
+
## Drive it from curl
|
|
45
|
+
|
|
46
|
+
The natural agent-driver shape but works for humans too:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
# Discover
|
|
50
|
+
curl http://127.0.0.1:4567/llms.txt
|
|
51
|
+
curl http://127.0.0.1:4567/openapi.json
|
|
52
|
+
|
|
53
|
+
# Start an experiment (capped -- demo accepts X-Demo-Cap-Run header
|
|
54
|
+
# as a stand-in for a real bearer-token-with-caps from a Tep::AuthOAuth2
|
|
55
|
+
# delegation flow)
|
|
56
|
+
curl -X POST http://127.0.0.1:4567/tools/start_experiment \
|
|
57
|
+
-H "Content-Type: application/json" \
|
|
58
|
+
-H "X-Demo-Cap-Run: 1" \
|
|
59
|
+
-d '{"name":"baseline","learning_rate":"1e-3","epochs":3}'
|
|
60
|
+
# -> started experiment id=1 (baseline)
|
|
61
|
+
|
|
62
|
+
# Advance an epoch
|
|
63
|
+
curl -X POST http://127.0.0.1:4567/tools/step_experiment \
|
|
64
|
+
-H "Content-Type: application/json" \
|
|
65
|
+
-d '{"id":1}'
|
|
66
|
+
# -> id=1 name=baseline lr=1e-3 status=running epoch=1/3 loss=0.90
|
|
67
|
+
|
|
68
|
+
# Snapshot
|
|
69
|
+
curl http://127.0.0.1:4567/resources/experiments/active
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## What this demo exercises
|
|
73
|
+
|
|
74
|
+
| Surface | What it does here |
|
|
75
|
+
|---|---|
|
|
76
|
+
| **`mcp_tool`** | All 4 tools declare typed params (`String` / `Integer`), descriptions, and (for the mutating two) a `caps: [:run_experiments]` gate. Tool bodies return `Tep::MCP.text(...)` for success and `Tep::MCP.error(...)` for the not-found path. |
|
|
77
|
+
| **`mcp_resource`** | 2 read-only fetches. Bodies return `Tep::MCP.resource_text(uri, body)`; mimeType defaults to `text/plain`. |
|
|
78
|
+
| **`/mcp` JSON-RPC** | Translator-generated dispatcher routes `initialize` / `tools/list` / `tools/call` / `resources/list` / `resources/read` / `notifications/initialized`. |
|
|
79
|
+
| **`/llms.txt`** | Auto-published markdown catalog. Both tools + resources sections, with the MCP endpoint URL + OpenAPI link in the header. |
|
|
80
|
+
| **`/openapi.json`** | Auto-published OpenAPI 3.0.3 spec for the HTTP-direct surface. Non-MCP agents and Swagger UI consume this directly. |
|
|
81
|
+
| **Capability gating** | `start_experiment` and `cancel_experiment` require `:run_experiments`; the read paths don't. Anonymous callers get an MCP `isError:true` response with `missing capability: run_experiments`. |
|
|
82
|
+
|
|
83
|
+
## What's NOT in this demo (intentionally)
|
|
84
|
+
|
|
85
|
+
- **Persistent state.** Module-level arrays only; counters reset
|
|
86
|
+
on restart. A real version would use `Tep::SQLite`.
|
|
87
|
+
- **Real training.** The "loss" series is synthetic. The point is
|
|
88
|
+
the agent-driver loop, not the ML.
|
|
89
|
+
- **Bearer-token auth.** The `X-Demo-Cap-Run` header shortcuts the
|
|
90
|
+
full `Tep::AuthOAuth2` + JWT flow that a production deployment
|
|
91
|
+
would use. The capability check itself (`req.identity.may?(...)`)
|
|
92
|
+
is real.
|
|
93
|
+
- **Streaming progress.** `tools/call` returns when the body
|
|
94
|
+
returns. Long-running experiments would benefit from MCP
|
|
95
|
+
`notifications/progress` over an SSE channel -- deferred
|
|
96
|
+
past chunk 5.4.
|
|
97
|
+
|
|
98
|
+
See `AGENTS.md` in this directory for the agent-facing surface
|
|
99
|
+
spec (the file convention agents look for at the repo root).
|