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.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. metadata +264 -0
data/lib/tep.rb ADDED
@@ -0,0 +1,981 @@
1
+ # Tep -- a Sinatra-flavoured framework that compiles to a native
2
+ # binary via Spinel.
3
+ #
4
+ # require_relative "../tep/lib/tep"
5
+ #
6
+ # class Root < Tep::Handler
7
+ # def handle(req, res)
8
+ # "<h1>hello, world</h1>"
9
+ # end
10
+ # end
11
+ # Tep.get "/", Root.new
12
+ #
13
+ # Tep.run!(4567, 1, false)
14
+ #
15
+ # Sinatra-classic source (with `do ... end` blocks) is supported via
16
+ # `bin/tep build app.rb`, which translates blocks into Handler
17
+ # subclasses before invoking spinel.
18
+
19
+ # --- stock-Ruby guard --------------------------------------------------
20
+ # tep's lib/ is Spinel-AOT source: it's COMPILED into a native binary
21
+ # (or inlined by `tep build`), not run under CRuby. A plain
22
+ # `require "tep"` on MRI/JRuby/TruffleRuby has no FFI runtime and would
23
+ # otherwise die with a cryptic `undefined method 'ffi_cflags'` deep in a
24
+ # sub-file. RUBY_ENGINE is defined on every stock Ruby but NOT in a
25
+ # Spinel binary, so this fires only under a real `require` and stays a
26
+ # dead no-op once compiled (it inlines harmlessly into every app).
27
+ if defined?(RUBY_ENGINE)
28
+ raise "tep is a Spinel-AOT framework, not a CRuby library -- " \
29
+ "`require \"tep\"` has no runtime here. tep compiles your " \
30
+ "Sinatra-style app to a native binary; build it with the " \
31
+ "translator:\n" \
32
+ " tep build app.rb && ./app -p 4567\n" \
33
+ "or declare `gem \"tep\"` in a bundler-spinel (spinelgems) " \
34
+ "Gemfile and let `spinel-compat vendor` inline it. " \
35
+ "See https://github.com/OriPekelman/tep"
36
+ end
37
+
38
+ require_relative "tep/version"
39
+ require_relative "tep/url"
40
+ require_relative "tep/multipart"
41
+ require_relative "tep/net"
42
+ require_relative "tep/agent_delegation"
43
+ require_relative "tep/identity"
44
+ # Auth + Broadcast + Presence data classes (no deps; storage on
45
+ # Tep::App references them, so they must load before app.rb).
46
+ require_relative "tep/auth_oauth2_client"
47
+ require_relative "tep/auth_oauth2_code"
48
+ require_relative "tep/broadcast_subscription"
49
+ require_relative "tep/presence_entry"
50
+ require_relative "tep/session"
51
+ require_relative "tep/request"
52
+ require_relative "tep/response"
53
+ require_relative "tep/cache"
54
+ require_relative "tep/handler"
55
+ require_relative "tep/filter"
56
+ require_relative "tep/streamer"
57
+ require_relative "tep/parser"
58
+ require_relative "tep/router"
59
+ require_relative "tep/app"
60
+ # Auth provider classes land after App so Tep::AuthFilter < Tep::Filter
61
+ # resolves and the install! helper can reach Tep::APP. References to
62
+ # Tep::Jwt / Tep::Json inside their method bodies resolve at runtime.
63
+ require_relative "tep/auth_bearer_token"
64
+ require_relative "tep/auth_session_cookie"
65
+ require_relative "tep/auth_oauth2"
66
+ require_relative "tep/auth"
67
+ require_relative "tep/broadcast"
68
+ require_relative "tep/presence"
69
+ require_relative "tep/live_view"
70
+ require_relative "tep/server"
71
+ require_relative "tep/server_scheduled"
72
+ require_relative "tep/sqlite"
73
+ require_relative "tep/pg"
74
+ require_relative "tep/json"
75
+ require_relative "tep/mcp"
76
+ require_relative "tep/logger"
77
+ require_relative "tep/jwt"
78
+ require_relative "tep/password"
79
+ require_relative "tep/security"
80
+ require_relative "tep/assets"
81
+ require_relative "tep/scheduler"
82
+ require_relative "tep/shell"
83
+ require_relative "tep/http"
84
+ require_relative "tep/proxy"
85
+ require_relative "tep/events"
86
+ require_relative "tep/llm"
87
+ require_relative "tep/openai_server"
88
+ require_relative "tep/websocket"
89
+ require_relative "tep/parallel"
90
+ require_relative "tep/job"
91
+
92
+ module Tep
93
+ # Helper: spinel won't infer types on an empty `{}`, so we seed
94
+ # with one entry then delete it. Used by Request/Response so
95
+ # users get the natural Hash[] / Hash[]= surface (Sinatra-style
96
+ # `params["name"]` works without a bespoke Bag wrapper).
97
+ # Holder for a Fiber so we can keep them in a typed array.
98
+ # Spinel's `[Fiber.new { ... }]` array literal infers IntArray
99
+ # (Fiber is a built-in pointer type, not a user class spinel
100
+ # tracks via PtrArray), so a one-attribute wrapper class is the
101
+ # cheapest way to put them in a homogeneous container.
102
+ class FiberSlot
103
+ attr_accessor :f
104
+ def initialize(f)
105
+ @f = f
106
+ end
107
+ end
108
+
109
+ def self.seed_fiber_noop
110
+ 0
111
+ end
112
+
113
+ # A canonical no-op fiber, used to type-seed Fiber-bearing
114
+ # collections without running anything user-visible. The body is
115
+ # a single method call (Fiber tests don't currently support
116
+ # arbitrary inline-block bodies in spinel).
117
+ def self.seed_fiber
118
+ Fiber.new { Tep.seed_fiber_noop }
119
+ end
120
+
121
+
122
+ def self.str_hash
123
+ h = {"" => ""}
124
+ h.delete("")
125
+ h
126
+ end
127
+
128
+ # str_find -- naive substring search returning the int position of
129
+ # `needle` in `s` starting from `start`, or -1 if not found.
130
+ #
131
+ # History: workaround for spinel `0210389` which made `String#index`
132
+ # return nil for not-found (was -1). spinel `28545ff` (matz/spinel#550)
133
+ # added int|nil narrowing after an explicit nil-guard, so the
134
+ # nil-side risk is upstream-resolved AND spinel supports the
135
+ # offset overload `s.index(needle, start)` directly (emits
136
+ # `sp_str_index_from_poly`). The helper stays solely for callsite
137
+ # ergonomics: the 17 callers all use `if x < 0` style int comparison
138
+ # (which can't narrow against int|nil under spinel's current
139
+ # narrowing model). Removing it would require a mechanical
140
+ # `< 0` -> `.nil?` refactor across http.rb / parser.rb / url.rb /
141
+ # jwt.rb / app.rb. Worth doing eventually; not urgent.
142
+ def self.str_find(s, needle, start)
143
+ nlen = needle.length
144
+ slen = s.length
145
+ pos = start
146
+ while pos <= slen - nlen
147
+ if s[pos, nlen] == needle
148
+ return pos
149
+ end
150
+ pos += 1
151
+ end
152
+ -1
153
+ end
154
+
155
+ # HTML-escape: minimum safe set for attribute and PCDATA contexts.
156
+ # Used by the build-time Mustache compiler for the default
157
+ # `{{var}}` (escaped) form. Char-by-char to avoid `gsub` (spinel's
158
+ # gsub coverage on string-typed receivers is uneven).
159
+ def self.h(s)
160
+ out = ""
161
+ i = 0
162
+ n = s.length
163
+ while i < n
164
+ c = s[i]
165
+ if c == "&"
166
+ out = out + "&amp;"
167
+ elsif c == "<"
168
+ out = out + "&lt;"
169
+ elsif c == ">"
170
+ out = out + "&gt;"
171
+ elsif c == "\""
172
+ out = out + "&quot;"
173
+ elsif c == "'"
174
+ out = out + "&#39;"
175
+ else
176
+ out = out + c
177
+ end
178
+ i += 1
179
+ end
180
+ out
181
+ end
182
+
183
+ # Session signing secret. Empty by default, which disables session
184
+ # writes (the Set-Cookie path no-ops). Set at app load time:
185
+ #
186
+ # Tep.session_secret = ENV.fetch("TEP_SESSION_SECRET")
187
+ #
188
+ # Stored on the APP instance (spinel doesn't reliably type-track
189
+ # module-level `@@cvars` or globals).
190
+
191
+ APP = App.new
192
+
193
+ def self.session_secret; APP.session_secret; end
194
+ def self.session_secret=(v); APP.set_session_secret(v); end
195
+
196
+ # Inbound TLS (tep#148 phase 2). Point these at a PEM cert + key and
197
+ # Tep::Server terminates HTTPS itself; unset (default) = plain HTTP.
198
+ # Tep.tls_cert = "cert.pem"; Tep.tls_key = "key.pem"
199
+ def self.tls_cert; APP.tls_cert; end
200
+ def self.tls_cert=(v); APP.set_tls_cert(v); end
201
+ def self.tls_key; APP.tls_key; end
202
+ def self.tls_key=(v); APP.set_tls_key(v); end
203
+
204
+ # Spinel infers method parameter types from concrete call sites.
205
+ # If a user app never calls Tep.before / Tep.not_found / etc.,
206
+ # spinel falls back to int and the underlying set_* assignment
207
+ # mismatches the typed ivar. Force-calling each setter here with
208
+ # the canonical default ensures the parameter type is locked in
209
+ # regardless of which DSL methods the user app actually invokes.
210
+ APP.set_static_root("")
211
+ APP.set_before(Filter.new)
212
+ APP.set_after(Filter.new)
213
+ APP.set_auth_filter(Filter.new)
214
+ APP.set_auth_bearer_secret("")
215
+ # Broadcast PG-backend setter seeds. enable_pg_backend reaches
216
+ # these via set_broadcast_pg_conn / _channel / _enabled when a
217
+ # connect succeeds; the empty-conninfo seed below short-circuits
218
+ # before getting there, so we exercise the setters directly.
219
+ APP.set_broadcast_pg_enabled(0)
220
+ APP.set_broadcast_pg_channel("")
221
+ APP.set_broadcast_pg_conn(PG::Connection.new(""))
222
+ APP.set_not_found(Handler.new)
223
+ # Type-seeding: methods that may not be called by a given user app
224
+ # would otherwise default their param C types to mrb_int and
225
+ # mismatch the typed ivars they touch.
226
+ _tep_seed_res = Response.new
227
+ _tep_seed_res.set_cookie("", "", str_hash)
228
+ APP.set_session_secret("")
229
+ APP.set_tls_cert("")
230
+ APP.set_tls_key("")
231
+ _tep_seed_sess = Session.new
232
+ _tep_seed_sess.load_from("", "")
233
+ _tep_seed_sess.to_cookie_value("")
234
+ _tep_seed_sess.set("a", "")
235
+ _tep_seed_sess.get("a")
236
+ _tep_seed_sess.has?("a")
237
+ _tep_seed_res.start_stream(Streamer.new)
238
+ _tep_seed_stream = Stream.new(0)
239
+ _tep_seed_res.streamer.pump(_tep_seed_stream)
240
+ _tep_seed_stream.write("") # pin the parameter type to :str
241
+ Tep.h("") # pin Tep.h(s)'s param to :str
242
+ # Multipart parser: pin all param types so the server-side
243
+ # branches that call Tep::Multipart.parse have proper signatures
244
+ # even when no user app exercises multipart on its own.
245
+ Tep::Multipart.parse("", "")
246
+ Tep::Multipart.extract_boundary("")
247
+ Tep::Multipart.extract_field_name("")
248
+ _tep_seed_res.start_websocket("", Tep::WebSocket::Driver.new(0))
249
+
250
+ # AuthOAuth2 type-seeding. Every public cmeth needs at least one
251
+ # top-level call so spinel locks the param C types in compile
252
+ # units that don't otherwise exercise OAuth2 (e.g. test_llm.rb
253
+ # builds an app that never touches Tep::AuthOAuth2; without
254
+ # these seeds spinel defaults the params to mrb_int and the
255
+ # AuthOAuth2Client / AuthOAuth2Code constructor calls inside
256
+ # the methods mismatch the typed ivars).
257
+ _tep_seed_oauth2_caps = [:_seed]
258
+ _tep_seed_oauth2_caps.delete_at(0)
259
+ Tep::AuthOAuth2.register_client("_seed", "", "", _tep_seed_oauth2_caps)
260
+ Tep::AuthOAuth2.unregister_client("_seed")
261
+ Tep::AuthOAuth2.find_client("_seed")
262
+ _tep_seed_oauth2_code = Tep::AuthOAuth2.issue_code("_seed", "_seed", "", 0)
263
+ Tep::AuthOAuth2.exchange_code(_tep_seed_oauth2_code, "_seed", 0)
264
+
265
+ # Broadcast type-seeding. Same pattern: pin every cmeth's param C
266
+ # types so compile units that don't otherwise exercise pub/sub
267
+ # still get correct signatures.
268
+ _tep_seed_broadcast_sub = Tep::Broadcast.subscribe("_seed", -1)
269
+ Tep::Broadcast.subscribe_ws("_seed", -1)
270
+ Tep::Broadcast.publish("_seed", "")
271
+ Tep::Broadcast.subscribers_for("_seed")
272
+ Tep::Broadcast.unsubscribe(_tep_seed_broadcast_sub)
273
+ Tep::Broadcast.unsubscribe_fd(-1)
274
+ Tep::Broadcast.subscriber_count
275
+ Tep::Broadcast.clear
276
+
277
+ # Broadcast PG-backend seeds. enable_pg_backend("", "") tries to
278
+ # open a PG connection -- empty conninfo behaves the same as the
279
+ # PG::Connection.new("") seed above: connect fails, returns -1.
280
+ # The point is to pin parameter types on every cmeth.
281
+ Tep::Broadcast.enable_pg_backend("", "")
282
+ Tep::Broadcast.poll_pg_once(0)
283
+ Tep::Broadcast.disable_pg_backend
284
+ Tep::Broadcast.encode_wire("", "")
285
+ Tep::Broadcast.deliver_wire_local("0:")
286
+ Tep::Broadcast.publish_local_only("_seed", "")
287
+ # The new PG::Connection LISTEN/NOTIFY method seeds live further
288
+ # down with the rest of the PG seeds, where _tep_seed_pg_conn is
289
+ # already defined.
290
+
291
+ # Presence type-seeding. Same pattern as Broadcast: pin every
292
+ # cmeth's param C types so compile units that don't otherwise
293
+ # touch Presence still get correct signatures. track() requires
294
+ # a req with a populated identity -- construct a synthetic one.
295
+ _tep_seed_presence_caps = [:_seed]
296
+ _tep_seed_presence_caps.delete_at(0)
297
+ _tep_seed_presence_req = Tep::Request.new
298
+ _tep_seed_presence_req.identity = Tep::Identity.new(
299
+ "_seed", nil, _tep_seed_presence_caps)
300
+ Tep::Presence.track(_tep_seed_presence_req, "_seed", -1)
301
+ Tep::Presence.find_entry("_seed", -1)
302
+ Tep::Presence.list("_seed")
303
+ Tep::Presence.count("_seed")
304
+ Tep::Presence.count_humans("_seed")
305
+ Tep::Presence.count_agents("_seed")
306
+ Tep::Presence.count_filtered("_seed", :both)
307
+ Tep::Presence.set_status("_seed", -1, :busy, "", 0)
308
+ Tep::Presence.clear_status("_seed", -1)
309
+ Tep::Presence.untrack("_seed", -1)
310
+ Tep::Presence.untrack_by_fd(-1)
311
+ Tep::Presence.clear
312
+ # Diff + auto-expiry seeds (chunk 3.2).
313
+ Tep::Presence.diff_topic("_seed")
314
+ _tep_seed_presence_entry = Tep::PresenceEntry.new(
315
+ "_seed", "_seed", :human, "", -1, 0)
316
+ Tep::Presence.encode_diff("join", _tep_seed_presence_entry)
317
+ Tep::Presence.publish_diff("join", _tep_seed_presence_entry)
318
+ Tep::Presence.sweep_expired_status
319
+ # PG mirror seeds (chunk 3.3). enable_pg_mirror("") fails the
320
+ # connect cleanly (-1) but still pins param types.
321
+ Tep::Presence.enable_pg_mirror("")
322
+ Tep::Presence.schema_sql
323
+ Tep::Presence.mirror_insert(_tep_seed_presence_entry)
324
+ Tep::Presence.mirror_delete("_seed", -1)
325
+ Tep::Presence.mirror_status("_seed", -1, :available, "", 0)
326
+ Tep::Presence.list_global("_seed")
327
+ Tep::Presence.count_global("_seed")
328
+ Tep::Presence.worker_schema_sql
329
+ Tep::Presence.heartbeat
330
+ Tep::Presence.prune_stale_workers(90)
331
+ Tep::Presence.disable_pg_mirror
332
+ # Same APP-setter-via-constant pattern as the broadcast_pg_conn
333
+ # seed: PG::Connection.new can't run inside App#initialize
334
+ # (Tep::APP is mid-construction; sched_current read segfaults).
335
+ APP.set_presence_pg_enabled(0)
336
+ APP.set_presence_pg_worker_id("")
337
+ APP.set_presence_pg_conn(PG::Connection.new(""))
338
+
339
+ # LiveView type-seeding (chunk 4.1). The render_page + dispatch_event
340
+ # cmeths get pinned via top-level calls; the base-class mount /
341
+ # render / handle_event imeths are pinned via a single noop
342
+ # instance call so subclass dispatch widens cleanly.
343
+ _tep_seed_live_view = Tep::LiveView.new
344
+ _tep_seed_live_view_req = Tep::Request.new
345
+ _tep_seed_live_view.mount(_tep_seed_live_view_req)
346
+ _tep_seed_live_view.render
347
+ _tep_seed_live_view.handle_event("", "", _tep_seed_live_view_req)
348
+ _tep_seed_live_view.dispatch_event_json("{}", _tep_seed_live_view_req)
349
+ _tep_seed_live_view.topic
350
+ _tep_seed_live_view.broadcast_render
351
+ _tep_seed_live_view.handle_presence_diff("{}")
352
+ _tep_seed_live_view.apply_presence_diff_json("{}")
353
+ Tep::LiveView.render_page("", "")
354
+
355
+ # SQLite type-seeding. Each method below pins a parameter type
356
+ # (or pulls the FFI return into use) so spinel emits the correct
357
+ # signatures even for apps that include the require but don't hit
358
+ # every method. We open an anonymous in-memory database, run a
359
+ # tiny round-trip, then close -- the leak is one malloc'd handle
360
+ # per process at startup, which exits with the worker.
361
+ _tep_seed_db = Tep::SQLite.new
362
+ if _tep_seed_db.open(":memory:")
363
+ _tep_seed_db.exec("CREATE TABLE _seed (k TEXT, v INTEGER)")
364
+ _tep_seed_db.prepare("INSERT INTO _seed (k, v) VALUES (?, ?)")
365
+ _tep_seed_db.bind_str(1, "")
366
+ _tep_seed_db.bind_int(2, 0)
367
+ _tep_seed_db.step
368
+ _tep_seed_db.finalize
369
+ _tep_seed_db.last_rowid
370
+ _tep_seed_db.prepare("SELECT k, v FROM _seed")
371
+ _tep_seed_db.step
372
+ _tep_seed_db.col_str(0)
373
+ _tep_seed_db.col_int(1)
374
+ _tep_seed_db.col_count
375
+ _tep_seed_db.reset
376
+ _tep_seed_db.finalize
377
+ _tep_seed_db.first_str("SELECT k FROM _seed", "")
378
+ _tep_seed_db.first_int("SELECT v FROM _seed", "")
379
+ # Pin the prepare_cached param type so apps that don't call it
380
+ # still see the FFI shape (`Sqlite.tep_sqlite_prepare_cached(int,
381
+ # str)`) at module-load. Cache hit / miss / reuse paths are
382
+ # exercised by test/test_sqlite_cached.rb at runtime.
383
+ _tep_seed_db.prepare_cached("SELECT k FROM _seed")
384
+ _tep_seed_db.step
385
+ _tep_seed_db.finalize
386
+ _tep_seed_db.close
387
+ end
388
+
389
+ # PG type-seeding. PG::Connection.new("") returns a connection-
390
+ # failed instance (@pgh=-1) rather than raising, so this is safe
391
+ # at module load regardless of whether libpq has a reachable
392
+ # server. The point is to pin parameter / return types on every
393
+ # public Connection / Result method so apps that don't exercise
394
+ # one method still compile cleanly.
395
+ _tep_seed_pg_conn = PG::Connection.new("")
396
+ _tep_seed_pg_conn.connected?
397
+ _tep_seed_pg_conn.status
398
+ _tep_seed_pg_conn.transaction_status
399
+ _tep_seed_pg_conn.server_version
400
+ _tep_seed_pg_conn.error_message
401
+ _tep_seed_pg_conn.escape_string("")
402
+ _tep_seed_pg_conn.escape_identifier("")
403
+ _tep_seed_pg_conn.escape_literal("")
404
+ _tep_seed_pg_conn.last_sqlstate = ""
405
+ _tep_seed_pg_conn.last_error_message = ""
406
+ _tep_seed_pg_conn.last_result_rh = -1
407
+ # Async surface seed -- calling these on a failed-conn instance
408
+ # is harmless (the C shim short-circuits on conn slot < 1).
409
+ _tep_seed_pg_conn.async_exec("")
410
+ _tep_seed_pg_seed_arr = [""]
411
+ _tep_seed_pg_seed_arr.delete_at(0)
412
+ _tep_seed_pg_conn.async_exec_params("", _tep_seed_pg_seed_arr)
413
+ # Async connect cmeth. Returns -1 for empty conninfo from a
414
+ # non-scheduled context (the shim's PQconnectStart-then-FAILED
415
+ # path), which is type-equivalent to the success path.
416
+ PG::Connection.async_connect("")
417
+ # LISTEN / NOTIFY surface (Tep::Broadcast PG backend lands here).
418
+ _tep_seed_pg_conn.listen("_seed")
419
+ _tep_seed_pg_conn.unlisten("_seed")
420
+ _tep_seed_pg_conn.notify("_seed", "")
421
+ _tep_seed_pg_conn.poll_notification(0)
422
+ _tep_seed_pg_conn.last_notify_channel
423
+ _tep_seed_pg_conn.last_notify_payload
424
+ _tep_seed_pg_res = PG::Result.new(-1)
425
+ _tep_seed_pg_res.ntuples
426
+ _tep_seed_pg_res.nfields
427
+ _tep_seed_pg_res.fname(0)
428
+ _tep_seed_pg_res.fnumber("")
429
+ _tep_seed_pg_res.ftype(0)
430
+ _tep_seed_pg_res.fformat(0)
431
+ _tep_seed_pg_res.fmod(0)
432
+ _tep_seed_pg_res.getvalue(0, 0)
433
+ _tep_seed_pg_res.getisnull(0, 0)
434
+ _tep_seed_pg_res.getlength(0, 0)
435
+ _tep_seed_pg_res.value(0, 0)
436
+ _tep_seed_pg_res.error_field(67)
437
+ _tep_seed_pg_res.cmd_status
438
+ _tep_seed_pg_res.cmd_tuples
439
+ _tep_seed_pg_res.error_message
440
+ _tep_seed_pg_res.sql_state
441
+ _tep_seed_pg_res.fields
442
+ _tep_seed_pg_res.values
443
+ _tep_seed_pg_res.column_values(0)
444
+ _tep_seed_pg_res.clear
445
+ _tep_seed_pg_conn.close
446
+ # Pool seed -- size 0 so we don't try to open real conns at load.
447
+ _tep_seed_pg_pool = PG::Pool.new("", 0)
448
+ _tep_seed_pg_pool.healthy?
449
+ _tep_seed_pg_pool.available
450
+ _tep_seed_pg_pool.size
451
+ _tep_seed_pg_pool.set_checkout_timeout_ms(0)
452
+ _tep_seed_pg_pool.close_all
453
+ # NB: don't checkout/checkin against the size-0 seed pool; it'd
454
+ # spin until timeout. The seed has @free.length=0 forever.
455
+
456
+ # Tep::Json type-seeding. Pin every public method's parameter
457
+ # types so an app that uses one method but not another still
458
+ # compiles cleanly. Calls have no side effects beyond producing
459
+ # discardable strings.
460
+ Tep::Json.escape("")
461
+ Tep::Json.quote("")
462
+ Tep::Json.encode_pair_str("", "")
463
+ Tep::Json.encode_pair_int("", 0)
464
+ _tep_seed_str_h = Tep.str_hash
465
+ _tep_seed_str_h["k"] = "v"
466
+ Tep::Json.from_str_hash(_tep_seed_str_h)
467
+ _tep_seed_int_h = {"" => 0}
468
+ _tep_seed_int_h.delete("")
469
+ _tep_seed_int_h["k"] = 1
470
+ Tep::Json.from_int_hash(_tep_seed_int_h)
471
+
472
+ # Tep::Logger seed -- pin parameter types for every method even
473
+ # when an app uses one but not another. The level-name string
474
+ # ("info") and the messages ("") pin the :str shape; the file-
475
+ # path setter pins to_file's :str arg.
476
+ _tep_seed_logger = Tep::Logger.new
477
+ _tep_seed_logger.set_level("info")
478
+ _tep_seed_logger.to_file("")
479
+ _tep_seed_logger.to_stderr
480
+ _tep_seed_logger.debug("")
481
+ _tep_seed_logger.info("")
482
+ _tep_seed_logger.warn("")
483
+ _tep_seed_logger.error("")
484
+ Tep::Logger.level_value("info")
485
+
486
+ # Tep::Jwt seed -- pin every method's :str arg types. The
487
+ # secret + payload are blank but the call shapes pin the FFI
488
+ # signature dispatch.
489
+ Tep::Jwt.encode_hs256("", "")
490
+ Tep::Jwt.verify_hs256("", "")
491
+ Tep::Jwt.decode_payload("")
492
+ Tep::Jwt.verify_and_decode("", "")
493
+ Tep::Jwt.timing_safe_eq("", "")
494
+
495
+ # Tep::Password seed -- one cheap PBKDF2 round at startup, just
496
+ # to pin every method's parameter types. iters=1 keeps the cost
497
+ # negligible.
498
+ _tep_seed_pwd_hash = Tep::Password.hash("seed")
499
+ Tep::Password.verify("seed", _tep_seed_pwd_hash)
500
+ Tep::Password.split4("a$b$c$d")
501
+
502
+ # Tep::Security seeding -- pin the filter classes' params.
503
+ _tep_seed_cors = Tep::Security::Cors.new
504
+ _tep_seed_cors.set_origin("")
505
+ _tep_seed_cors.set_allowed_verbs("")
506
+ _tep_seed_cors.set_allowed_headers("")
507
+ _tep_seed_cors.set_max_age(0)
508
+ _tep_seed_hdrs = Tep::Security::Headers.new
509
+ _tep_seed_hdrs.set_hsts(0)
510
+
511
+ # Tep::Assets seed -- pin the str args before any user-supplied
512
+ # _add calls land. The asset hash starts empty; user apps that
513
+ # have `<app>/assets/` get _add lines emitted by bin/tep at
514
+ # build time.
515
+ Tep::Assets._add("", "", "")
516
+ Tep::Assets.has?("")
517
+ _tep_seed_assets_res = Response.new
518
+ Tep::Assets.serve("", _tep_seed_assets_res)
519
+
520
+ # Tep::Scheduler seed -- run every public method once so spinel
521
+ # pins the param/return types. The seed Fiber's body is an
522
+ # immediately-finishing Tep.seed_fiber_noop, so resume + tick are
523
+ # cheap. io_wait gets seeded outside any fiber context, which
524
+ # exercises the idx < 0 single-shot poll path (fd=-1 returns
525
+ # immediately on most kernels with a POLLNVAL, which we collapse
526
+ # to 0 in sphttp_poll_ready).
527
+ _tep_seed_fiber = Tep.seed_fiber
528
+ Tep::Scheduler.spawn_fiber(_tep_seed_fiber)
529
+ Tep::Scheduler.tick(0)
530
+ Tep::Scheduler.poll_round(0)
531
+ Tep::Scheduler.any_io_waiter
532
+ Tep::Scheduler.alive_count
533
+ Tep::Scheduler.next_wake
534
+ Tep::Scheduler.run_until_empty
535
+ Tep::Scheduler.run_for(0)
536
+ Tep::Scheduler.pause(0)
537
+ Tep::Scheduler.io_wait(-1, Tep::Scheduler::READ, 0)
538
+ Tep::Scheduler.clear
539
+
540
+ # Tep::Shell seed -- pin :str args at the FFI boundary.
541
+ Tep::Shell.run(":")
542
+ Tep::Shell.run_limited(":", 1)
543
+ Tep::Shell.read("/etc/hostname")
544
+ Tep::Shell.read_limited("/etc/hostname", 64)
545
+
546
+ # Tep::Url seed -- the new split_url has to land at compile time.
547
+ Tep::Url.split_url("http://x/")
548
+
549
+ # Tep::Http seed -- every public method gets one canonical call so
550
+ # spinel pins the param types. The URL "http://127.0.0.1:1/" won't
551
+ # connect; send_req returns the empty Response, which is the
552
+ # type-pinning behaviour we want without any real I/O.
553
+ _tep_seed_http_headers = Tep.str_hash
554
+ _tep_seed_http_headers["k"] = "v"
555
+ Tep::Http.send_req("GET", "http://127.0.0.1:1/", "", _tep_seed_http_headers)
556
+ Tep::Http.get("http://127.0.0.1:1/")
557
+ Tep::Http.post("http://127.0.0.1:1/", "")
558
+ Tep::Http.put("http://127.0.0.1:1/", "")
559
+ Tep::Http.patch("http://127.0.0.1:1/", "")
560
+ Tep::Http.delete("http://127.0.0.1:1/")
561
+ Tep::Http.head("http://127.0.0.1:1/")
562
+ Tep::Http.empty_headers
563
+ # Pool seed (chunk 6.7a). Pin the (str, int) -> int / (int, str, int) -> int
564
+ # arities so the FFI bindings resolve. Each call site exercises one
565
+ # primitive against the empty pool -- harmless at boot.
566
+ Tep::Http::Pool.claim("127.0.0.1", 1)
567
+ Tep::Http::Pool.release(-1, "127.0.0.1", 1)
568
+ Tep::Http::Pool.close_idle(30)
569
+ Tep::Http::Pool.stats
570
+ _tep_seed_http = Tep::Http.new("http://127.0.0.1:1")
571
+ _tep_seed_http.set_header("k", "v")
572
+ _tep_seed_http.do_get("/")
573
+ _tep_seed_http.do_post("/", "")
574
+ _tep_seed_http.do_put("/", "")
575
+ _tep_seed_http.do_patch("/", "")
576
+ _tep_seed_http.do_delete("/")
577
+ _tep_seed_http.do_head("/")
578
+ # parse_response and index_from are internal; let spinel infer
579
+ # their types from the send_req call site rather than seeding
580
+ # separately (which widens `out` to poly).
581
+
582
+ # Tep::Proxy seed -- a base-class Proxy instance pins the handler
583
+ # slot + every overridable hook signature so subclass call sites
584
+ # in user code resolve cleanly (same idiom as set_before(Filter.new)
585
+ # and the Parallel/Job seeds). handle() exercises the full forward
586
+ # path against a dead port (status 0 -> 502), which fails fast like
587
+ # the Tep::Http seed above.
588
+ _tep_seed_proxy = Tep::Proxy.new("http://127.0.0.1:1")
589
+ _tep_seed_proxy_req = Tep::Request.new
590
+ _tep_seed_proxy_res = Response.new
591
+ _tep_seed_proxy_ureq = Tep::Proxy::UpstreamRequest.new
592
+ _tep_seed_proxy_ureq.set_header("k", "v")
593
+ _tep_seed_proxy.rewrite_path("/")
594
+ _tep_seed_proxy.before_forward(_tep_seed_proxy_req, _tep_seed_proxy_res, _tep_seed_proxy_ureq)
595
+ _tep_seed_proxy.after_forward(_tep_seed_proxy_req, Tep::Http::Response.new, _tep_seed_proxy_res)
596
+ _tep_seed_proxy.handle(_tep_seed_proxy_req, _tep_seed_proxy_res)
597
+ Tep::Proxy.hop_by_hop?("connection")
598
+ # Streaming surface (chunk 6.2). Pin the hook signatures + the
599
+ # UpstreamHead parser + the proxy's non-IO pump helpers. run_stream,
600
+ # pump and read_upstream_head are NOT called here -- they do blocking
601
+ # io_wait on a real fd; their param/return types self-pin from their
602
+ # bodies, and ProxyStreamer flows into res.start_stream from
603
+ # start_streaming_forward (statically reachable from handle), which
604
+ # wires ProxyStreamer.pump (-> run_stream) into the Streamer dispatch.
605
+ # 6.4 per-request upstream picker. Pin the Tep::Request param +
606
+ # the :str return so subclass overrides resolve cleanly.
607
+ _tep_seed_proxy.pick_upstream(_tep_seed_proxy_req)
608
+ # 6.6 body-cap accessors. Type-pin the int setters / getters so
609
+ # subclass overrides + block-DSL setters compile.
610
+ _tep_seed_proxy.max_request_body_bytes = 1
611
+ _tep_seed_proxy.max_response_body_bytes = 1
612
+ # 6.5 retry policy. Pin the RetryPolicy slot via instantiation +
613
+ # the hook return type via a call to #retry_policy(req).
614
+ _tep_seed_retry_policy = Tep::Proxy::RetryPolicy.new
615
+ _tep_seed_retry_policy.max_attempts = 1
616
+ _tep_seed_retry_policy.base_backoff_ms = 0
617
+ _tep_seed_retry_policy.backoff_multiplier = 2
618
+ _tep_seed_retry_policy.retry_on_status = [502, 503, 504]
619
+ # Float-seconds setter (#133). Pin the Float -> int(ms) lowering
620
+ # so the conversion call site resolves.
621
+ _tep_seed_retry_policy.base_backoff_secs = 0.0
622
+ _tep_seed_retry_policy.base_backoff_secs
623
+ # Pin Sock.sphttp_sleep_ms's :int param so the backoff call site
624
+ # resolves (called from Tep::Proxy#handle).
625
+ Sock.sphttp_sleep_ms(0)
626
+ # Tep::Json.get_float seed (#133). Pin the (String, String) -> Float
627
+ # surface so callers (CompletionsHandler temperature/top_p,
628
+ # backends that parse their own bodies) resolve cleanly.
629
+ Tep::Json.get_float("{\"temperature\":0.7}", "temperature")
630
+ _tep_seed_retry_policy.backoff_for(0)
631
+ _tep_seed_retry_policy.retriable?(502)
632
+ _tep_seed_proxy.retry_policy(_tep_seed_proxy_req)
633
+ _tep_seed_proxy.stream_request?(_tep_seed_proxy_req)
634
+ _tep_seed_pstats = Tep::Proxy::StreamStats.new
635
+ _tep_seed_pchunk = Tep::Proxy::StreamChunk.new("data: x\n\n")
636
+ _tep_seed_proxy.on_stream_chunk(_tep_seed_pchunk, _tep_seed_stream, _tep_seed_pstats)
637
+ _tep_seed_proxy.on_stream_end(_tep_seed_proxy_req, _tep_seed_stream, _tep_seed_pstats)
638
+ _tep_seed_proxy.drain_events(_tep_seed_stream, _tep_seed_pstats, "data: x\n\n")
639
+ _tep_seed_proxy.dispatch_one(_tep_seed_stream, _tep_seed_pstats, "data: x\n\n")
640
+ _tep_seed_uhead = Tep::Proxy::UpstreamHead.new
641
+ _tep_seed_uhead.fill_from("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nTransfer-Encoding: chunked")
642
+ _tep_seed_pstreamer = Tep::Proxy::ProxyStreamer.new
643
+ _tep_seed_pstreamer.proxy = _tep_seed_proxy
644
+ _tep_seed_proxy_res.start_stream(_tep_seed_pstreamer)
645
+
646
+ # Tep::Events seed (toy/v1 emitter). Seeded with a disabled ("")
647
+ # path so the guards short-circuit before any File I/O at boot;
648
+ # the JSON-building bodies + sphttp_iso8601_utc call still compile
649
+ # statically, and the call args pin every param type.
650
+ _tep_seed_events = Tep::Events.new("")
651
+ _tep_seed_events.enabled?
652
+ _tep_seed_events.run_start("host", "cpu", "model", "/path", "{}")
653
+ _tep_seed_events.inference("model", 0, 0, 0, "{}")
654
+ _tep_seed_events.record_error
655
+ _tep_seed_events.run_end("ok")
656
+ # #128: aggregated run_end (parent reads JSONL + sums). The seed
657
+ # path is "" so the call short-circuits before any File I/O at
658
+ # boot; this exists to pin the method's surface area so spinel's
659
+ # codegen emits it.
660
+ _tep_seed_events.run_end_aggregated("completed")
661
+ _tep_seed_events.rel_t
662
+ Sock.sphttp_iso8601_utc(0)
663
+
664
+ # Tep::Llm::OpenAI::Server seed (Battery 7, chunk 7.1a). Pin the
665
+ # Backend slot + interface + the ModelsHandler dispatch through
666
+ # APP.openai_backend. serve! is NOT called here -- it mounts a route
667
+ # (a global side effect); its types self-pin from its body.
668
+ _tep_seed_oai_backend = Tep::Llm::OpenAI::Backend.new
669
+ Tep::APP.set_openai_backend(_tep_seed_oai_backend)
670
+ # 7.1c openai_events slot. Re-uses _tep_seed_events (already declared
671
+ # above as the Tep::Events seed). Pins APP.openai_events so the route
672
+ # handlers' `Tep::APP.openai_events.inference(...)` dispatch resolves.
673
+ Tep::APP.set_openai_events(_tep_seed_events)
674
+ Tep::Llm::OpenAI::Server.use(_tep_seed_oai_backend)
675
+ _tep_seed_oai_backend.list_models
676
+ _tep_seed_oai_backend.supports_chat?
677
+ _tep_seed_oai_backend.device_kind
678
+ _tep_seed_oai_backend.supports_embeddings?
679
+ _tep_seed_oai_models = Tep::Llm::OpenAI::ModelsHandler.new
680
+ _tep_seed_oai_models.handle(_tep_seed_proxy_req, _tep_seed_proxy_res)
681
+ # 7.1b /v1/completions surface.
682
+ Tep::Json.get_int_array("{}", "prompt")
683
+ _tep_seed_oai_sampling = Tep::Llm::OpenAI::Sampling.new
684
+ _tep_seed_oai_sampling.max_tokens = 0
685
+ _tep_seed_oai_sampling.temperature = 1.0
686
+ _tep_seed_oai_sampling.top_p = 1.0
687
+ _tep_seed_oai_comp = Tep::Llm::OpenAI::Completion.new
688
+ _tep_seed_oai_backend.generate_from_tokens("m", Tep::Json.get_int_array("{}", "prompt"), _tep_seed_oai_sampling)
689
+ _tep_seed_oai_completions = Tep::Llm::OpenAI::CompletionsHandler.new
690
+ _tep_seed_oai_completions.handle(_tep_seed_proxy_req, _tep_seed_proxy_res)
691
+ # Chat completions skeleton (POST /v1/chat/completions). Default
692
+ # backend.supports_chat? is false -> ChatCompletionsHandler returns
693
+ # 501; the override path (supports_chat? = true, chat_completion
694
+ # overridden) dispatches to the backend's chat_completion. Pin
695
+ # Backend#chat_completion's `req` param + the ChatCompletionsHandler
696
+ # dispatch through APP.openai_backend.
697
+ _tep_seed_oai_backend.chat_completion(_tep_seed_proxy_req)
698
+ # parse_messages helper. Type-pin the [Tep::Llm::Message] return so
699
+ # backends that call `messages = Tep::Llm::OpenAI.parse_messages(...)`
700
+ # get a typed array.
701
+ Tep::Llm::OpenAI.parse_messages(
702
+ "{\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}")
703
+ Tep::Llm::OpenAI.find_obj_key_str("{}", 0, 2, "role")
704
+ _tep_seed_oai_chat = Tep::Llm::OpenAI::ChatCompletionsHandler.new
705
+ _tep_seed_oai_chat.handle(_tep_seed_proxy_req, _tep_seed_proxy_res)
706
+ # 7.2 streaming completions: pin StreamSink + CompletionsStreamer
707
+ # slots + exercise the backend's generate_stream_from_tokens(sink)
708
+ # arity so the param type resolves. emit_token is called against a
709
+ # fd=-1 Stream -- the seed never runs the actual write (the streamer
710
+ # pump is never invoked at boot), it just gives spinel the surface.
711
+ _tep_seed_oai_stream = Tep::Stream.new(-1)
712
+ _tep_seed_oai_sink = Tep::Llm::OpenAI::StreamSink.new
713
+ _tep_seed_oai_sink.out = _tep_seed_oai_stream
714
+ _tep_seed_oai_sink.model = "m"
715
+ _tep_seed_oai_sink.completion_count
716
+ # emit_token call site pins the `piece` parameter to String. fd=-1
717
+ # makes the underlying sphttp_write_chunk a harmless EBADF at boot.
718
+ _tep_seed_oai_sink.emit_token("seed")
719
+ _tep_seed_oai_backend.generate_stream_from_tokens(
720
+ "m", Tep::Json.get_int_array("{}", "prompt"), _tep_seed_oai_sampling, _tep_seed_oai_sink)
721
+ _tep_seed_oai_cstreamer = Tep::Llm::OpenAI::CompletionsStreamer.new
722
+ _tep_seed_oai_cstreamer.model = "m"
723
+ _tep_seed_oai_cstreamer.token_ids = Tep::Json.get_int_array("{}", "prompt")
724
+ _tep_seed_oai_cstreamer.sampling = _tep_seed_oai_sampling
725
+ _tep_seed_oai_cstreamer.prompt_tokens = 0
726
+ _tep_seed_oai_cstreamer.t0 = 0
727
+ _tep_seed_oai_cstreamer.request_id = ""
728
+ _tep_seed_oai_cstreamer.principal_id = ""
729
+ _tep_seed_proxy_res.start_stream(_tep_seed_oai_cstreamer)
730
+
731
+ # #127 chat streaming: ChatStreamSink + ChatCompletionsStreamer.
732
+ # Mirror the 7.2 seed shape so spinel pins the sink's emit_*
733
+ # arities + the streamer's accessor slots.
734
+ _tep_seed_oai_chat_sink = Tep::Llm::OpenAI::ChatStreamSink.new
735
+ _tep_seed_oai_chat_sink.out = _tep_seed_oai_stream
736
+ _tep_seed_oai_chat_sink.model = "m"
737
+ _tep_seed_oai_chat_sink.completion_count
738
+ _tep_seed_oai_chat_sink.emit_role_prelude("assistant")
739
+ _tep_seed_oai_chat_sink.emit_token("seed")
740
+ _tep_seed_oai_chat_sink.emit_finish("stop")
741
+ _tep_seed_oai_backend.chat_completion_stream(_tep_seed_proxy_req, _tep_seed_oai_chat_sink)
742
+ _tep_seed_oai_chat_streamer = Tep::Llm::OpenAI::ChatCompletionsStreamer.new
743
+ _tep_seed_oai_chat_streamer.req_ref = _tep_seed_proxy_req
744
+ _tep_seed_oai_chat_streamer.model = "m"
745
+ _tep_seed_oai_chat_streamer.prompt_tokens = 0
746
+ _tep_seed_oai_chat_streamer.t0 = 0
747
+ _tep_seed_oai_chat_streamer.request_id = ""
748
+ _tep_seed_oai_chat_streamer.principal_id = ""
749
+ _tep_seed_proxy_res.start_stream(_tep_seed_oai_chat_streamer)
750
+
751
+ # Tep::Shell.write seed.
752
+ Tep::Shell.write("/dev/null", "")
753
+
754
+ # Tep::Parallel seed -- a base-class Parallel instance pins the
755
+ # `worker` slot type to ParallelWorker; subclass call sites at
756
+ # user code get auto-cast (same idiom as set_before(Filter.new)).
757
+ _tep_seed_par = Tep::Parallel.new(Tep::ParallelWorker.new)
758
+ _tep_seed_par_items = [""]
759
+ _tep_seed_par_items.delete_at(0)
760
+ _tep_seed_par.map_processes(_tep_seed_par_items)
761
+ _tep_seed_par.each_process(_tep_seed_par_items)
762
+ Tep::Parallel.scratch_dir
763
+
764
+ # Tep::Job seed -- pin every public-surface method's parameter
765
+ # types against an in-memory SQLite so the leak is one malloc'd
766
+ # handle per process at startup. The base `perform(arg)` is also
767
+ # pinned to :str so subclass overrides resolve cleanly.
768
+ Tep::Job.init_schema(":memory:")
769
+ _tep_seed_job = Tep::Job.new
770
+ _tep_seed_job.perform("")
771
+ Tep::Job.enqueue("seed", "", ":memory:")
772
+ Tep::Job.fetch_next(":memory:")
773
+ Tep::Job.mark_done(":memory:", 0, "")
774
+ Tep::Job.mark_failed(":memory:", 0)
775
+ _tep_seed_str_arr = [""]
776
+ _tep_seed_str_arr.delete_at(0)
777
+ Tep::Json.from_str_array(_tep_seed_str_arr)
778
+ _tep_seed_int_arr = [0]
779
+ _tep_seed_int_arr.delete_at(0)
780
+ Tep::Json.from_int_array(_tep_seed_int_arr)
781
+ Tep::Json.get_str("{}", "")
782
+ Tep::Json.get_int("{}", "")
783
+ Tep::Json.has_key?("{}", "")
784
+
785
+ # Tep::MCP seeds (chunk 5.1). Tools register at compile time via
786
+ # bin/tep's mcp_tool DSL; the runtime helpers below are the
787
+ # shared shapes the translator-emitted dispatcher leans on.
788
+ # Seed both Result-construction paths so neither widens via the
789
+ # "no concrete caller -> int default" route. Read text + is_error
790
+ # off both so attr_accessor types pin to String / Integer.
791
+ _tep_seed_mcp_result = Tep::MCP.text("seed")
792
+ _tep_seed_mcp_result_err = Tep::MCP.error("seed")
793
+ Tep::MCP.nested_extract("{}", "")
794
+ Tep::MCP.initialize_envelope(0, "", "")
795
+ Tep::MCP.tools_list_envelope(0, "[]")
796
+ Tep::MCP.tools_call_envelope(0, "", 0)
797
+ Tep::MCP.tools_call_envelope(0, "", 1)
798
+ Tep::MCP.unknown_tool_envelope(0, "")
799
+ Tep::MCP.method_not_found_envelope(0, "")
800
+ # Resource seeds (chunk 5.3). resource_text gives us a typed
801
+ # ResourceContent for the resources/read path; the envelope
802
+ # builders take scalars to keep param-type inference tight.
803
+ _tep_seed_mcp_rc = Tep::MCP.resource_text("seed-uri", "seed-text")
804
+ _tep_seed_mcp_rc_uri = _tep_seed_mcp_rc.uri
805
+ _tep_seed_mcp_rc_mime = _tep_seed_mcp_rc.mime
806
+ _tep_seed_mcp_rc_text = _tep_seed_mcp_rc.text
807
+ Tep::MCP.resources_list_envelope(0, "[]")
808
+ Tep::MCP.resources_read_envelope(0, "", "text/plain", "")
809
+ Tep::MCP.unknown_resource_envelope(0, "")
810
+
811
+ # Tep::Llm seeds. attr_accessor return types default to mrb_int
812
+ # if spinel sees no concrete callsite -- and Tep::Llm.build_request_body
813
+ # passes msg.role / msg.content into Tep::Json.quote(String) which
814
+ # then mismatches. Pin Message + Response attrs to String, and
815
+ # run one full encode + parse round-trip so the static analyzer
816
+ # sees every public method called with concrete types.
817
+ _tep_seed_llm_msg = Tep::Llm::Message.new("user", "")
818
+ _tep_seed_llm_msg.role = ""
819
+ _tep_seed_llm_msg.content = ""
820
+ _tep_seed_llm_msgs = [_tep_seed_llm_msg]
821
+ Tep::Llm.build_request_body("", "", _tep_seed_llm_msgs)
822
+ _tep_seed_llm_resp = Tep::Llm::Response.new
823
+ _tep_seed_llm_resp.content = ""
824
+ _tep_seed_llm_resp.role = ""
825
+ _tep_seed_llm_resp.stop_reason = ""
826
+ _tep_seed_llm_http_res = Tep::Http::Response.new
827
+ Tep::Llm.parse_response(_tep_seed_llm_http_res)
828
+ Tep::Llm.extract_str_field("", "", 0)
829
+ _tep_seed_llm_client = Tep::Llm.new("")
830
+ _tep_seed_llm_client.set_model("")
831
+ _tep_seed_llm_client.set_api_key("")
832
+ _tep_seed_llm_client.set_system_prompt("")
833
+ # Streaming surface seeds. The chat_stream signature wants a
834
+ # Tep::Stream `out_stream` -- using fd=0 (stdin) for the seed
835
+ # never executes the .write path here (this block runs at module
836
+ # init; the chat_stream call below is type-seed-only and would
837
+ # need a real connection to actually fire). Tep::Llm::StreamState
838
+ # likewise pinned via attr writes.
839
+ _tep_seed_llm_state = Tep::Llm::StreamState.new
840
+ _tep_seed_llm_state.acc = ""
841
+ _tep_seed_llm_state.leftover = ""
842
+ _tep_seed_llm_state.done = false
843
+ _tep_seed_llm_stream = Tep::Stream.new(0)
844
+ Tep::Llm.consume_sse_events(_tep_seed_llm_stream, _tep_seed_llm_state)
845
+ Tep::Llm.dechunk_consume("")
846
+ Tep::Llm.dechunk_leftover("")
847
+ Tep::Llm.dechunk_pass("")
848
+ Tep::Llm.drain_sse_buf("", _tep_seed_llm_stream, "")
849
+ Tep::Llm.hex_to_int("")
850
+
851
+ # Tep::WebSocket seeds. Pins frame/handshake/driver/connection
852
+ # surfaces to concrete typed callsites so the analyzer doesn't
853
+ # default param types to mrb_int.
854
+ _tep_seed_ws_frame = Tep::WebSocket::Frame.new(true, 1, "")
855
+ _tep_seed_ws_frame.fin = true
856
+ _tep_seed_ws_frame.opcode = 1
857
+ _tep_seed_ws_frame.payload = ""
858
+ _tep_seed_ws_frame.encode_unmasked
859
+ Tep::WebSocket::Frame.byte_to_chr(0)
860
+ Tep::WebSocket::Frame.parse_from_buf(0, 0)
861
+ Tep::WebSocket::Frame.reserved_opcode?(0)
862
+ Tep::WebSocket::Frame.control_opcode?(0)
863
+ _tep_seed_ws_pr = Tep::WebSocket::ParseResult.new
864
+ _tep_seed_ws_pr.outcome = ""
865
+ _tep_seed_ws_pr.consumed = 0
866
+ _tep_seed_ws_pr.close_code = 0
867
+ _tep_seed_ws_pr.frame = _tep_seed_ws_frame
868
+ _tep_seed_ws_hsres = Tep::WebSocket::Handshake::Result.new
869
+ _tep_seed_ws_hsres.valid = false
870
+ _tep_seed_ws_hsres.reason = ""
871
+ _tep_seed_ws_hsres.accept_key = ""
872
+ Tep::WebSocket::Handshake.build_response("", "")
873
+ Tep::WebSocket::Handshake.icontains("", "")
874
+ Tep::WebSocket::Handshake.downcase("")
875
+ Tep::WebSocket::Handshake.trim("")
876
+ _tep_seed_ws_csv = Tep::WebSocket::Handshake.split_csv("")
877
+ _tep_seed_ws_handler = Tep::WebSocket::Handler.new
878
+ _tep_seed_ws_event = Tep::WebSocket::Event.new
879
+ _tep_seed_ws_event.data = ""
880
+ _tep_seed_ws_event.code = 0
881
+ _tep_seed_ws_event.reason = ""
882
+ _tep_seed_ws_handler.handle_event(_tep_seed_ws_event)
883
+ _tep_seed_ws_drv = Tep::WebSocket::Driver.new(0)
884
+ _tep_seed_ws_drv.set_max_frame_size(0)
885
+ _tep_seed_ws_drv.set_subprotocol("")
886
+ _tep_seed_ws_drv.set_on_open(_tep_seed_ws_handler)
887
+ _tep_seed_ws_drv.set_on_message(_tep_seed_ws_handler)
888
+ _tep_seed_ws_drv.set_on_close(_tep_seed_ws_handler)
889
+ _tep_seed_ws_drv.set_on_ping(_tep_seed_ws_handler)
890
+ _tep_seed_ws_drv.set_on_pong(_tep_seed_ws_handler)
891
+ _tep_seed_ws_drv.set_on_error(_tep_seed_ws_handler)
892
+ _tep_seed_ws_drv.text("")
893
+ _tep_seed_ws_drv.binary("")
894
+ _tep_seed_ws_drv.ping("")
895
+ _tep_seed_ws_drv.pong("")
896
+ _tep_seed_ws_drv.close(1000, "")
897
+ Tep::WebSocket::Driver.encode_close_payload(0, "")
898
+ _tep_seed_ws_conn = Tep::WebSocket::Connection.new(_tep_seed_ws_drv)
899
+ _tep_seed_ws_conn.set_idle_timeout(0)
900
+ _tep_seed_ws_cs = Tep::WebSocket::ConnectionState.new
901
+ _tep_seed_ws_cs.start = 0
902
+ _tep_seed_ws_cs.avail = 0
903
+
904
+ # ---------------- DSL ----------------
905
+ # Spinel emits every defined method whether called or not, and
906
+ # infers parameter types from concrete call sites; methods nobody
907
+ # calls fall back to int parameters that mismatch the typed ivars
908
+ # they assign. So the v0.1 surface only exposes what the bundled
909
+ # demos actually use; richer DSL methods (before/after/not_found)
910
+ # are layered on as the demos grow to exercise them.
911
+
912
+ def self.get(pattern, handler); APP.add_route("GET", pattern, handler); end
913
+ def self.post(pattern, handler); APP.add_route("POST", pattern, handler); end
914
+ def self.put(pattern, handler); APP.add_route("PUT", pattern, handler); end
915
+ def self.patch(pattern, handler); APP.add_route("PATCH", pattern, handler); end
916
+ def self.delete(pattern, handler); APP.add_route("DELETE", pattern, handler); end
917
+
918
+
919
+ def self.public_dir(root)
920
+ APP.set_static_root(root)
921
+ end
922
+
923
+ def self.before(filter)
924
+ APP.set_before(filter)
925
+ end
926
+
927
+ def self.after(filter)
928
+ APP.set_after(filter)
929
+ end
930
+
931
+ def self.not_found(handler)
932
+ APP.set_not_found(handler)
933
+ end
934
+
935
+ # ARGV access only emits `sp_argv` when used at top level, so the
936
+ # translator emits the option-parsing loop itself before calling
937
+ # `Tep.run!`. The `scheduled` flag picks between the prefork
938
+ # blocking server (default) and the fiber-per-connection
939
+ # Tep::Server::Scheduled (opt-in via `set :scheduler, :scheduled`
940
+ # in the app source, or `-s` on the CLI). At the next major tep
941
+ # release Scheduled becomes the default and Blocking is deleted;
942
+ # the parallel-classes period exists only to make the rollback
943
+ # path obvious during the transition.
944
+ #
945
+ # Single dispatch method (rather than parallel run! / run_scheduled!)
946
+ # because spinel's codegen mis-declares heap-cell parameters when
947
+ # two same-arity sibling methods are called from an if/else --
948
+ # both branches reference `quiet` as a heap-cell but only the first
949
+ # path declares it. Bundling the decision inside one method
950
+ # sidesteps the codegen miss.
951
+ #
952
+ # `scheduled` defaults to false so apps that ship the historical
953
+ # 3-arg call (Tep.run!(port, workers, quiet)) keep building. Spinel
954
+ # accepts the call without the 4th arg only because it supports
955
+ # default-value params; without this, the 3-arg call silently
956
+ # miscompiled (matz/spinel arity-warning shape, tep#13).
957
+ def self.run!(port, workers, quiet, scheduled = false)
958
+ if scheduled
959
+ Server::Scheduled.new(APP).run(port, workers, quiet)
960
+ else
961
+ Server.new(APP).run(port, workers, quiet)
962
+ end
963
+ end
964
+
965
+ # Called by the SERVER PARENT (workers>1) or the single process
966
+ # (workers=1) at SIGTERM/SIGINT, AFTER the worker children have
967
+ # exited. Children no longer emit run_end themselves -- #128 moved
968
+ # the emission here so a multi-worker deployment writes exactly ONE
969
+ # run_end with aggregated stats from the events.jsonl, not N per
970
+ # worker.
971
+ #
972
+ # reason: "completed" -- matches toy/v1 vocabulary (was "ok"; #115).
973
+ # Cheap when nothing is configured: openai_events is seeded with an
974
+ # empty path, whose enabled? short-circuits.
975
+ def self.on_shutdown
976
+ if APP.openai_events.enabled?
977
+ APP.openai_events.run_end_aggregated("completed")
978
+ end
979
+ 0
980
+ end
981
+ end