syntropy 0.38.1 → 0.40.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/README.md +2 -0
  4. data/TODO.md +55 -144
  5. data/cmd/console.rb +2 -4
  6. data/cmd/new/template/config/production.rb +2 -1
  7. data/cmd/new.rb +1 -1
  8. data/cmd/serve.rb +29 -4
  9. data/cmd/test.rb +12 -4
  10. data/examples/agent/Dockerfile +25 -0
  11. data/examples/agent/Gemfile +3 -0
  12. data/examples/agent/README.md +40 -0
  13. data/examples/agent/TODO.md +5 -0
  14. data/examples/agent/app/_layout/default.rb +15 -0
  15. data/examples/agent/app/_lib/storage.rb +13 -0
  16. data/examples/agent/app/_schema/2026-01-01-initial.rb +9 -0
  17. data/examples/agent/app/assets/agent.js +63 -0
  18. data/examples/agent/app/assets/minigfm.js +207 -0
  19. data/examples/agent/app/assets/model.js +45 -0
  20. data/examples/agent/app/assets/style.css +25 -0
  21. data/examples/agent/app/assets/tools.js +74 -0
  22. data/examples/agent/app/assets/ui.js +50 -0
  23. data/examples/agent/app/index.rb +19 -0
  24. data/examples/agent/app/test.rb +7 -0
  25. data/examples/agent/config/Caddyfile +5 -0
  26. data/examples/agent/config/development.rb +5 -0
  27. data/examples/agent/config/production.rb +5 -0
  28. data/examples/agent/config/test.rb +5 -0
  29. data/examples/agent/docker-compose.yml +71 -0
  30. data/examples/agent/response_example.json +108 -0
  31. data/examples/agent/test/test_app.rb +14 -0
  32. data/examples/blog/app/foo.rb +1 -0
  33. data/examples/blog/app/posts/[id]/edit.rb +3 -3
  34. data/examples/blog/app/posts/[id]/index.rb +8 -10
  35. data/examples/blog/app/posts/index.rb +6 -8
  36. data/examples/blog/app/posts/new.rb +3 -3
  37. data/examples/blog/config/production.rb +2 -1
  38. data/lib/syntropy/app.rb +12 -60
  39. data/lib/syntropy/applets/builtin/auto_refresh/watch.js +1 -1
  40. data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +7 -0
  41. data/lib/syntropy/controller_extensions.rb +1 -1
  42. data/lib/syntropy/dev_mode.rb +8 -3
  43. data/lib/syntropy/http/client.rb +17 -1
  44. data/lib/syntropy/http/client_connection.rb +16 -0
  45. data/lib/syntropy/http/io_extensions.rb +32 -4
  46. data/lib/syntropy/http/server.rb +55 -24
  47. data/lib/syntropy/http/server_connection.rb +72 -2
  48. data/lib/syntropy/logger.rb +8 -0
  49. data/lib/syntropy/markdown.rb +139 -11
  50. data/lib/syntropy/module_loader.rb +110 -68
  51. data/lib/syntropy/request/request_info.rb +8 -0
  52. data/lib/syntropy/request/response.rb +76 -6
  53. data/lib/syntropy/request/validation.rb +6 -0
  54. data/lib/syntropy/storage/connection_pool.rb +34 -0
  55. data/lib/syntropy/test.rb +14 -5
  56. data/lib/syntropy/version.rb +1 -1
  57. data/lib/syntropy.rb +9 -2
  58. data/syntropy.gemspec +4 -4
  59. data/test/fixtures/app/_layout/default.rb +1 -1
  60. data/test/fixtures/app/_layout/kuku.rb +8 -0
  61. data/test/fixtures/app/_lib/circular/a.rb +2 -0
  62. data/test/fixtures/app/_lib/circular/b.rb +2 -0
  63. data/test/fixtures/app/_lib/circular/c.rb +2 -0
  64. data/test/fixtures/app/mod/concurrent.rb +9 -0
  65. data/test/test_app.rb +1 -1
  66. data/test/test_caching.rb +3 -3
  67. data/test/test_markdown.rb +268 -0
  68. data/test/test_module_loader.rb +32 -4
  69. data/test/test_request.rb +7 -0
  70. data/test/test_response.rb +30 -0
  71. data/test/test_schema.rb +1 -0
  72. metadata +40 -11
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1ec61f53807ee4049d4c5e04ae42f66f3b4824733a04cad245d150a0c72f0acc
4
- data.tar.gz: ee5b563302d15d6a8d58fc7460636b5f402f726f730c42799862ceb92cafa42f
3
+ metadata.gz: 96429a90f9e886ffafc25c1e4c88a46157fb79751d21fdbed7af10252ff5d16e
4
+ data.tar.gz: 040ebeaf507ada9e986847b7630739f9451c88d5384c736fd632f55d4928bff2
5
5
  SHA512:
6
- metadata.gz: 30d9fcc1b9d0db54e4faa8fa009e547d69e8b6b1beeb3684995b9c27c8f22ebffdffc5c3ec0000495aac235d21b6d61b8cb57a2e305941fc3ec00276c12d283c
7
- data.tar.gz: 8453a4dfc06cba6e3cd741fb1260618af3825366cc937256cb0d2614c6dc520d5da983309c47010e493acbbae66d8a4cc50e898d3cc6723d9189364914c6579a
6
+ metadata.gz: f75c54e095c15c1742786f712062142619a0e7c23ee22295f3dc93825d476027ab26a962054ad3cf868f8d89045af2c3c29733b648d9042ba2594595783ab234
7
+ data.tar.gz: d60a7f2d439b615db027c9f678ab4fba2aae38deb1c68b9d9e5b29bdbe22d282f7d14e1111458c97e3b7ad004eef1a428714376d40bd1370fa668e08765fe739
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ # 0.40.0 2026-08-24
2
+
3
+ - Update UringMachine
4
+
5
+ # 0.39.0 2026-06-21
6
+
7
+ - Add support for embedded Papercraft snippets in Markdown files
8
+ - Reimplement Markdown rendering
9
+ - Increase cache-control max-age to one week
10
+ - Add support for relative redirects, add `Request#rel`
11
+ - Add support for concurrent loading, add mutex lock to module loader
12
+ - Fix `Syntropy.load_config`
13
+ - Read module source file using UringMachine
14
+ - Raise on circular module dependency
15
+ - Streamline CLI options across commands
16
+
1
17
  # 0.38.1 2026-06-13
2
18
 
3
19
  - Fix builtin app, controller extensions
data/README.md CHANGED
@@ -52,6 +52,8 @@ Syntropy is based on:
52
52
  - [Extralite](https://github.com/digital-fabric/extralite) a fast and innovative
53
53
  SQLite wrapper for Ruby.
54
54
 
55
+ Syntropy is 100% brain-coded.
56
+
55
57
  ## Examples
56
58
 
57
59
  To get a taste of some of Syntropy's capabilities, you can run the included
data/TODO.md CHANGED
@@ -1,8 +1,54 @@
1
1
  ## Immediate
2
2
 
3
- - [ ] Controllers
4
- - [ ] Streamline names of ready-made control methods:
5
- - [ ] dispatch_json_rpc
3
+ - [ ] Nicer logging in development mode:
4
+
5
+ Nice terminal formatting with colors:
6
+
7
+ ```
8
+ sharon@nf1:~/tmp/capatest2$ npx serve dist
9
+
10
+ ┌───────────────────────────────────────────┐
11
+ │ │
12
+ │ Serving! │
13
+ │ │
14
+ │ - Local: http://localhost:3000 │
15
+ │ - Network: http://192.168.0.106:3000 │
16
+ │ │
17
+ │ Copied local address to clipboard! │
18
+ │ │
19
+ └───────────────────────────────────────────┘
20
+
21
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /
22
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 42 ms
23
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/index-edfizW3i.js
24
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/index-PVjztr5e.css
25
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 3 ms
26
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 12 ms
27
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/p-Sh0ICmPV-D227nRX-.js
28
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 3 ms
29
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/p-C4t5ymfq-4gquBJ2r.js
30
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 6 ms
31
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 GET /assets/p-D6Ynv7Xh-CRAF8up3.js
32
+ HTTP 7/6/2026 8:04:23 PM 127.0.0.1 Returned 200 in 4 ms
33
+ ^C
34
+ INFO Gracefully shutting down. Please wait...
35
+ ```
36
+
37
+ - [ ] Ability to load modules from builtin applet
38
+ - [ ] Can we mount them on the app's module loader?
39
+
40
+ Why we need that? The use case is making use of a default pub/sub instance
41
+
42
+ - [ ] Pub/sub
43
+ - [ ] Ruby side
44
+ - [ ] JS side
45
+ - [ ] Reimplement `auto_refresh` using a *default* event bus provided by
46
+ Syntropy
47
+
48
+ - [ ] An alternative to the pub/sub design - add streaming responses to the
49
+ JSON/jS API (using SSE).
50
+
51
+ ## Collections
6
52
 
7
53
  - [ ] Collection - treat directories and files as collections of data.
8
54
 
@@ -34,7 +80,6 @@
34
80
 
35
81
  - [ ] Improve serving of static files:
36
82
  - [ ] support for compression
37
- - [ ] support for caching headers
38
83
  - [ ] add `Request#render_static_file(route, fn)
39
84
 
40
85
  ## Missing for a first public release
@@ -43,21 +88,18 @@
43
88
  - [ ] Website
44
89
  - [v] Frontend part of JSON API
45
90
  - [v] Auto-refresh page when file changes
46
- - [ ] SQLite database capabilities
47
- - [ ] Stores
48
- - [ ] KV store (with TTL)
49
91
  - [v] Examples
50
92
  - [v] Reactive app - counter or some other simple app showing interaction with
51
93
  server
52
- - [ ] blog
94
+ - [v] blog
53
95
 
54
96
  ## Testing facilities
55
97
 
56
98
  - What do we need to test?
57
- - Routes
58
- - Route responses
59
- - Changes to state / DB
60
- - Rendered HTML - presence of certain markup / elements / text
99
+ - [v] Routes
100
+ - [v] Route responses
101
+ - [v] Changes to state / DB
102
+ - [ ] Rendered HTML - presence of certain markup / elements / text
61
103
 
62
104
  ## Support for applets
63
105
 
@@ -93,138 +135,7 @@ class Syntropy::Admin < Syntropy::App
93
135
  end
94
136
  ```
95
137
 
96
- ## Response: cookies and headers
97
-
98
- We need a way to inject cookies into the response. This probably should be done
99
- in the TP2 code:
100
-
101
- ```ruby
102
- @@default_set_cookie_attr = 'HttpOnly'
103
- def self.default_set_cookie_attr=(v)
104
- @@default_set_cookie_attr = v
105
- end
106
-
107
- def set_cookie(key, value, attr = @@default_set_cookie_attr)
108
- @buffered_headers ||= +''
109
- @buffered_headers << format(
110
- "Set-Cookie: %<key>s=%<value>s; %<attr>s\n",
111
- key:, value:, attr:
112
- )
113
- end
114
-
115
- def set_headers(headers)
116
- @buffered_headers ||= +''
117
- @buffered_headers << format_headers(headers)
118
- end
119
-
120
- ...
121
-
122
- req.set_cookie('at', 'foobar', 'SameSite=none; Secure; HttpOnly')
123
- ```
124
-
125
- ## Middleware
126
-
127
- Some standard middleware:
128
-
129
- - request rewriter
130
- - logger
131
- - auth
132
- - selector + terminator
133
-
134
- ```Ruby
135
- # For the chainable DSL shown below, we need to create a custom class:
136
- class Syntropy::Middleware::Selector
137
- def initialize(select_proc, terminator_proc = nil)
138
- @select_proc = select_proc
139
- @terminator_proc = terminator_proc
140
- end
141
-
142
- def to_proc
143
- ->(req, proc) {
144
- @select_proc.(req) ? @terminator_proc.(req) : proc(req)
145
- }
146
- end
147
-
148
- def terminate(&proc)
149
- @terminator_proc = proc
150
- end
151
- end
152
- ```
153
-
154
- ```Ruby
155
- # a _site.rb file can be used to wrap a whole app
156
- # site/_site.rb
157
-
158
- # this means we route according to the host header, with each
159
- export Syntropy.route_by_host
160
-
161
- # we can also rewrite requests:
162
- rewriter = Syntropy
163
- .select { it.host =~ /^tolkora\.(org|com)$/ }
164
- .terminate { it.redirect_permanent('https://tolkora.net') }
165
-
166
- # This is actuall a pretty interesting DSL design:
167
- # a chain of operations that compose functions. So, we can select a
168
- export rewriter.wrap(default_app)
169
-
170
- # composing
171
- export rewriter.wrap(Syntropy.some_custom_app.wrap(app))
172
-
173
- # or maybe
174
- export rewriter << some_other_middleware << app
175
- ```
176
-
177
- ## CLI tool for setting up a site repo:
178
-
179
- ```bash
180
- # clone a newly created repo
181
- ~/repo$ git clone https://github.com/foo/bar
182
- ...
183
- ~/repo$ syntropy setup bar
184
-
185
- (syntropy banner)
186
-
187
- Setting up Syntropy project in /home/sharon/repo/bar:
188
-
189
- bar/
190
- bin/
191
- start
192
- stop
193
- restart
194
- console
195
- server
196
- docker-compose.yml
197
- Dockerfile
198
- Gemfile
199
- proxy/
200
- README.md
201
- site/
202
- _layout/
203
- default.rb
204
- _lib/
205
- about.md
206
- articles/
207
- long-form.md
208
- assets/
209
- js/
210
- css/
211
- style.css
212
- img/
213
- syntropy.png
214
- index.rb
215
- ```
216
-
217
- Some of the files might need templating, but we can maybe do without, or at
218
- least make it as generic as possible.
219
-
220
- `syntropy setup` steps:
221
-
222
- 1. Verify existence of target directory
223
- 2. Copy files from Syntropy template to target directory
224
- 3. Do chmod +x for bin/*
225
- 4. Do bundle install in the target directory
226
- 5. Show some information with regard to how to get started working with the
227
- repo
138
+ ## Some more CLI commands
228
139
 
229
140
  `syntropy provision` steps:
230
141
 
data/cmd/console.rb CHANGED
@@ -17,11 +17,11 @@ env = {
17
17
  parser = OptionParser.new do |o|
18
18
  o.banner = 'Usage: syntropy console [options]'
19
19
 
20
- o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
20
+ o.on('-a', '--app PATH', 'Set app directory (default: ./app)') do |path|
21
21
  env[:app_root] = path
22
22
  end
23
23
 
24
- o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
24
+ o.on('-c', '--config PATH', 'Set config directory (default: ./config)') do |path|
25
25
  env[:config_root] = path
26
26
  end
27
27
 
@@ -72,8 +72,6 @@ env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
72
72
  @app = Syntropy::App.load(env)
73
73
  @env = env
74
74
  @machine = env[:machine]
75
- @connection_pool = @app.connection_pool if @app.respond_to?(:connection_pool)
76
- @schema = @app.schema if @app.respond_to?(:schema)
77
75
  @module_loader = @app.module_loader
78
76
 
79
77
  require 'uringmachine/fiber_scheduler'
@@ -1,4 +1,5 @@
1
1
  export(
2
- storage:
2
+ storage: {
3
3
  path: ENV['DATABASE_PATH'] || 'storage/production.db'
4
+ }
4
5
  )
data/cmd/new.rb CHANGED
@@ -44,7 +44,7 @@ template_path = File.join(__dir__, 'new/template')
44
44
  begin
45
45
  `mkdir -p "#{path}"`
46
46
  system("cp -rv#{opts[:yes] ? '' : 'i'} #{template_path}/* \"#{path}/\"")
47
- puts "Your app is ready in #{path}"
47
+ puts "Your app is ready in #{full_path}"
48
48
  rescue => e
49
49
  p e
50
50
  p e.backtrace
data/cmd/serve.rb CHANGED
@@ -20,17 +20,16 @@ env = {
20
20
  parser = OptionParser.new do |o|
21
21
  o.banner = 'Usage: syntropy serve [options]'
22
22
 
23
- o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
23
+ o.on('-a', '--app PATH', 'Set app directory (default: ./app)') do |path|
24
24
  env[:app_root] = path
25
25
  end
26
26
 
27
- o.on('-b', '--bind BIND', String,
28
- 'Bind address (default: http://0.0.0.0:1234). You can specify this flag multiple times to bind to multiple addresses.') do
27
+ o.on('-b', '--bind ADDRESS', String, 'Bind address (default: 0.0.0.0:1234)') do
29
28
  env[:bind] ||= []
30
29
  env[:bind] << it
31
30
  end
32
31
 
33
- o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
32
+ o.on('-c', '--config PATH', 'Set config directory (default: ./config)') do |path|
34
33
  env[:config_root] = path
35
34
  end
36
35
 
@@ -100,4 +99,30 @@ Fiber.set_scheduler(UM::FiberScheduler.new(env[:machine]))
100
99
  require 'syntropy/dev_mode' if Syntropy.dev_mode
101
100
 
102
101
  app = Syntropy::App.load(env)
102
+
103
+ if env[:mode] == 'development'
104
+ @main_fiber = Fiber.current
105
+ env[:machine].spin do
106
+ @app = app
107
+ @env = env
108
+ @machine = env[:machine]
109
+ @module_loader = @app.module_loader
110
+
111
+ loop do
112
+ puts "Enter command: (c + Enter to open console, q + Enter to quit)"
113
+ command = $stdin.gets.chomp
114
+ case command
115
+ when 'c'
116
+ t = Thread.new {
117
+ require 'irb'
118
+ IRB.start
119
+ }
120
+ t.join
121
+ when 'q'
122
+ @machine.schedule(@main_fiber, UM::Terminate.new)
123
+ end
124
+ end
125
+ end
126
+ end
127
+
103
128
  Syntropy.run(env) { app.call(it) }
data/cmd/test.rb CHANGED
@@ -16,11 +16,11 @@ MINITEST_ARGV = []
16
16
  parser = OptionParser.new do |o|
17
17
  o.banner = 'Usage: syntropy test [options]'
18
18
 
19
- o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
19
+ o.on('-a', '--app PATH', 'Set app directory (default: ./app)') do |path|
20
20
  env[:app_root] = path
21
21
  end
22
22
 
23
- o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
23
+ o.on('-c', '--config PATH', 'Set config directory (default: ./config)') do |path|
24
24
  env[:config_root] = path
25
25
  end
26
26
 
@@ -29,6 +29,10 @@ parser = OptionParser.new do |o|
29
29
  exit
30
30
  end
31
31
 
32
+ o.on('-f', '--file PATH', 'Set test file') do |path|
33
+ env[:test_file] = path
34
+ end
35
+
32
36
  o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
33
37
  env[:mount_path] = path
34
38
  env[:builtin_applet_path] = File.join(path, '.syntropy')
@@ -82,7 +86,11 @@ Syntropy.load_config(env)
82
86
  $stdout.sync = true
83
87
  $stderr.sync = true
84
88
 
85
- Dir.glob("#{File.expand_path(env[:test_root])}/test_*.rb").each { require(it) }
89
+ if env[:test_file]
90
+ require(File.expand_path(env[:test_file]))
91
+ else
92
+ Dir.glob("#{File.expand_path(env[:test_root])}/test_*.rb").each { require(it) }
93
+ end
86
94
 
87
95
  def restart_on_file_change(machine, dir, restart_argv)
88
96
  machine.file_watch(dir, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) {
@@ -92,7 +100,7 @@ def restart_on_file_change(machine, dir, restart_argv)
92
100
  exec('ruby', __FILE__, *restart_argv)
93
101
  end
94
102
 
95
- Syntropy::Test.env = (env)
103
+ Syntropy::Test.global_env = env
96
104
  Minitest.run MINITEST_ARGV
97
105
 
98
106
  if env[:watch_mode]
@@ -0,0 +1,25 @@
1
+ FROM ruby:4.0-slim AS base
2
+ WORKDIR /syntropy
3
+ RUN apt-get update -qq && apt-get install -y \
4
+ curl \
5
+ build-essential \
6
+ libpq-dev \
7
+ libsqlite3-dev \
8
+ libssl-dev \
9
+ libyaml-dev \
10
+ pkg-config && \
11
+ rm -rf /var/lib/apt/lists /var/cache/apt/archives
12
+
13
+ ENV BUNDLE_PATH="/usr/local/bundle"
14
+ COPY Gemfile Gemfile.lock ./
15
+ RUN bundle install
16
+
17
+ RUN groupadd --system --gid 1000 syntropy && \
18
+ useradd syntropy --uid 1000 --gid 1000 --create-home --shell /bin/bash
19
+ USER 1000:1000
20
+
21
+ EXPOSE 1234
22
+
23
+ # Start the main process
24
+ # CMD ["bash"]
25
+ CMD ["bundle", "exec", "syntropy", "serve"]
@@ -0,0 +1,3 @@
1
+ source 'https://gem.coop'
2
+
3
+ gem 'syntropy', '~>0.39.0', path: '../..'
@@ -0,0 +1,40 @@
1
+ # My awesome Syntropy app
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ $ bundle install
7
+ ```
8
+
9
+ ## Running the web server
10
+
11
+ ```bash
12
+ $ bundle exec syntropy serve
13
+ ```
14
+
15
+ ## Starting the app console
16
+
17
+ ```bash
18
+ $ bundle exec syntropy console
19
+ ```
20
+
21
+ ### Running tests
22
+
23
+ ```bash
24
+ $ bundle exec syntropy tests
25
+ ```
26
+
27
+ ### Usage with Docker Compose
28
+
29
+ You can also run the app on Docker Compose:
30
+
31
+ ```bash
32
+ # run the app server
33
+ $ docker compose up
34
+
35
+ # run the console
36
+ $ docker compose run --remove-orphans console
37
+
38
+ # run tests
39
+ $ docker compose run --remove-orphans test
40
+ ```
@@ -0,0 +1,5 @@
1
+ - switch to JS modules, reorganize code
2
+ - bring it over to the Reality code
3
+ - Implement error response from tool (using try..catch)
4
+ - Implement OpenRouter OAuth integration (can it work on localhost?)
5
+ -
@@ -0,0 +1,15 @@
1
+ export template { |**props|
2
+ html {
3
+ head {
4
+ title "My awesome Syntropy web app"
5
+ meta charset: 'utf-8'
6
+ meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
7
+ link rel: "stylesheet", href: 'https://cdn.simplecss.org/simple.min.css'
8
+ # link rel: "stylesheet", type: "text/css", href: "/assets/style.css"
9
+ }
10
+ body {
11
+ render_children(**props)
12
+ auto_refresh!
13
+ }
14
+ }
15
+ }
@@ -0,0 +1,13 @@
1
+ export self
2
+
3
+ def connection_pool
4
+ @connection_pool ||= Storage::ConnectionPool.new(@machine, @env[:config][:storage][:path], 4)
5
+ end
6
+
7
+ def schema
8
+ Storage::Schema.new(module_loader: @module_loader, schema_root: '_schema')
9
+ end
10
+
11
+ def migrate!
12
+ schema.apply(connection_pool)
13
+ end
@@ -0,0 +1,9 @@
1
+ export ->(db) {
2
+ # db.execute <<~SQL
3
+ # create table posts (
4
+ # id integer primary key autoincrement,
5
+ # title text,
6
+ # body text
7
+ # )
8
+ # SQL
9
+ }
@@ -0,0 +1,63 @@
1
+ import * as UI from './ui.js';
2
+ import * as Model from './model.js';
3
+ import * as Tools from './tools.js';
4
+
5
+ export async function runPromptExchange(callHandler) {
6
+ const section = UI.createNewSection();
7
+ const prompt = await UI.askPrompt(section);
8
+ UI.addSectionEntryMarkdown(section, `*Prompt*: ${prompt}`);
9
+ const ctx = Model.buildExchangeBody(prompt);
10
+
11
+ while (true) {
12
+ UI.addPendingEntry(section);
13
+ const response = await Model.query(ctx);
14
+ UI.removePendingEntry(section);
15
+ if (!(await processResponse(section, ctx, response, callHandler)))
16
+ break;
17
+ }
18
+ }
19
+
20
+ async function processResponse(section, ctx, response, callHandler) {
21
+ const first_choice = response.choices[0];
22
+ if (first_choice.finish_reason == "tool_calls") {
23
+ Model.addMessage(ctx, first_choice.message);
24
+ const tool_calls = first_choice.message.tool_calls;
25
+ const tool_results = await performToolCalls(section, ctx, tool_calls, callHandler);
26
+ console.log("tool results", tool_results);
27
+ Model.mergeToolResults(ctx, tool_results);
28
+ return true;
29
+ }
30
+ else if (first_choice.finish_reason == "stop") {
31
+ UI.addSectionEntryMarkdown(section, first_choice.message.content);
32
+ return false;
33
+ }
34
+
35
+ }
36
+
37
+ async function performToolCalls(section, ctx, tool_calls, callHandler) {
38
+ const pending = tool_calls.map(async (c) => await runTool(section, ctx, callHandler, c));
39
+ return await Promise.all(pending);
40
+ }
41
+
42
+ async function runTool(section, ctx, callHandler, call) {
43
+ UI.addSectionEntryMarkdown(section, `Running \`${call.function.name}(${call.function.arguments})\``);
44
+ const name = call.function.name;
45
+ const args = JSON.parse(call.function.arguments);
46
+ try {
47
+ const res = await callHandler(name, args);
48
+ return {
49
+ id: call.id,
50
+ result: res
51
+ }
52
+ }
53
+ catch (e) {
54
+ return {
55
+ id: call.id,
56
+ error: e.message
57
+ }
58
+ }
59
+ }
60
+
61
+
62
+
63
+ runPromptExchange(Tools.toolHandler);