ask-app-server 0.1.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.
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ask
6
+ module AppServer
7
+ # Session store backed by Ask::State::Adapter.
8
+ #
9
+ # Stores session metadata and subscriptions in the state backend.
10
+ # AgentAdapter instances (which hold in-memory agent sessions) are
11
+ # kept separately and not persisted.
12
+ #
13
+ # Default backend is Ask::State::Memory (in-process, ephemeral).
14
+ # Pass Ask::State::Providers::SQLite for persistent storage:
15
+ #
16
+ # store = SessionStore.new(
17
+ # state: Ask::State::Providers::SQLite.new(path: "~/.ask-app-server/state.db")
18
+ # )
19
+ class SessionStore
20
+ SESSION_PREFIX = "session:"
21
+ SUBSCRIPTION_PREFIX = "sub:"
22
+ SESSION_LIST_KEY = "sessions"
23
+ EVENT_PREFIX = "events:"
24
+
25
+ def initialize(state: nil)
26
+ @state = state || Ask::State::Memory.new
27
+ @adapters = {} # session_id => AgentAdapter (in-memory only)
28
+ @mutex = Mutex.new
29
+ end
30
+
31
+ # Register a new session.
32
+ def add(session_id, adapter)
33
+ model = adapter.respond_to?(:instance_variable_get) ? adapter.instance_variable_get(:@model) : nil
34
+ metadata = {
35
+ sessionId: session_id,
36
+ model: model,
37
+ createdAt: adapter.respond_to?(:created_at) ? adapter.created_at.iso8601 : Time.now.iso8601
38
+ }
39
+
40
+ @mutex.synchronize do
41
+ existing = @state.get("#{SESSION_PREFIX}#{session_id}")
42
+ raise Ask::AppServer::SessionAlreadyExists, "Session #{session_id} already exists" if existing
43
+
44
+ @state.set("#{SESSION_PREFIX}#{session_id}", metadata)
45
+ @state.list_append(SESSION_LIST_KEY, session_id, max_length: 200)
46
+ @adapters[session_id] = adapter
47
+ end
48
+ end
49
+
50
+ # Remove a session.
51
+ def remove(session_id)
52
+ @mutex.synchronize do
53
+ _remove(session_id)
54
+ end
55
+ end
56
+
57
+ # Internal remove (no locking — caller must hold @mutex).
58
+ def _remove(session_id)
59
+ @state.delete("#{SESSION_PREFIX}#{session_id}")
60
+ @state.delete("#{SUBSCRIPTION_PREFIX}#{session_id}")
61
+ @state.delete("#{EVENT_PREFIX}#{session_id}")
62
+ @adapters.delete(session_id)
63
+ end
64
+
65
+ # Get a session adapter by ID.
66
+ def get(session_id)
67
+ @mutex.synchronize { @adapters[session_id] }
68
+ end
69
+
70
+ # List all sessions (summaries).
71
+ def list(limit: 20)
72
+ session_ids = @state.list_range(SESSION_LIST_KEY, 0, limit - 1)
73
+ session_ids.map do |sid|
74
+ metadata = @state.get("#{SESSION_PREFIX}#{sid}")
75
+ if metadata
76
+ adapter = @adapters[sid]
77
+ metadata.merge(
78
+ running: adapter&.running || false,
79
+ idle: adapter&.idle? || true
80
+ )
81
+ end
82
+ end.compact
83
+ end
84
+
85
+ # Subscribe to a session's events.
86
+ def subscribe(session_id, delivery_kind:)
87
+ @mutex.synchronize do
88
+ raise Ask::AppServer::SessionNotFound, "Session #{session_id} not found" unless @adapters.key?(session_id)
89
+ @state.set("#{SUBSCRIPTION_PREFIX}#{session_id}", { subscribed: true, deliveryKind: delivery_kind })
90
+ end
91
+ end
92
+
93
+ # Unsubscribe from a session.
94
+ def unsubscribe(session_id)
95
+ @mutex.synchronize do
96
+ @state.delete("#{SUBSCRIPTION_PREFIX}#{session_id}")
97
+ end
98
+ end
99
+
100
+ # Is the session subscribed?
101
+ def subscribed?(session_id)
102
+ sub = @state.get("#{SUBSCRIPTION_PREFIX}#{session_id}")
103
+ sub && sub[:subscribed]
104
+ end
105
+
106
+ # Count of active sessions.
107
+ def count
108
+ @adapters.size
109
+ end
110
+
111
+ # Clear all sessions.
112
+ def clear
113
+ @mutex.synchronize do
114
+ @adapters.each_key { |sid| _remove(sid) }
115
+ end
116
+ end
117
+
118
+ # Iterate over all session adapters.
119
+ def each(&block)
120
+ # Iterate over a snapshot to avoid holding the mutex during yield
121
+ snapshot = @mutex.synchronize { @adapters.values.dup }
122
+ snapshot.each(&block)
123
+ end
124
+
125
+ # Append an event to a session's event log.
126
+ def append_event(session_id, event)
127
+ @state.list_append("#{EVENT_PREFIX}#{session_id}", event, max_length: 1000)
128
+ end
129
+
130
+ # Get events for a session after a given sequence number.
131
+ def events_after(session_id, after_seq, limit: nil)
132
+ events = @state.list_range("#{EVENT_PREFIX}#{session_id}") || []
133
+ filtered = events.select { |e| e[:seq].to_i > after_seq.to_i }
134
+ limit ? filtered.first(limit) : filtered
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module AppServer
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module AppServer
5
+ class Error < StandardError; end
6
+ class ProtocolError < Error; end
7
+ class SessionNotFound < Error; end
8
+ class SessionAlreadyExists < Error; end
9
+ class SessionNotSubscribed < Error; end
10
+ class InvalidRequest < Error; end
11
+ class TimeoutError < Error; end
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ask/agent"
4
+ require "ask-tools-shell"
5
+
6
+ require_relative "ask/app_server/version"
7
+ require_relative "ask/app_server"
8
+ require_relative "ask/app_server/config"
9
+ require_relative "ask/app_server/event_translator"
10
+ require_relative "ask/app_server/agent_adapter"
11
+ require_relative "ask/app_server/permission_handler"
12
+ require_relative "ask/app_server/session_store"
13
+ require_relative "ask/app_server/session_manager"
14
+ require_relative "ask/app_server/server"
15
+ require_relative "ask/app_server/cli"
metadata ADDED
@@ -0,0 +1,201 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ask-app-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Kaka Ruto
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ask-agent
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ask-core
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ask-tools
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: ask-tools-shell
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0.1'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0.1'
68
+ - !ruby/object:Gem::Dependency
69
+ name: ask-state-providers
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0.1'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0.1'
82
+ - !ruby/object:Gem::Dependency
83
+ name: minitest
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '5.25'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '5.25'
96
+ - !ruby/object:Gem::Dependency
97
+ name: mocha
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.1'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.1'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rake
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '13.0'
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '13.0'
124
+ - !ruby/object:Gem::Dependency
125
+ name: simplecov
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.22'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.22'
138
+ - !ruby/object:Gem::Dependency
139
+ name: ostruct
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
152
+ description: Exposes Ask::Agent::Session behind the standard app-server JSON-RPC/stdio
153
+ protocol. Drop-in compatible with clients that speak the ZCode/Codex app-server
154
+ protocol — Telegram bots, AI SDK providers, IDE extensions, and headless automation.
155
+ email:
156
+ - kaka@myrrlabs.com
157
+ executables:
158
+ - ask-app-server
159
+ extensions: []
160
+ extra_rdoc_files: []
161
+ files:
162
+ - CHANGELOG.md
163
+ - LICENSE
164
+ - README.md
165
+ - bin/ask-app-server
166
+ - lib/ask-app-server.rb
167
+ - lib/ask/app_server.rb
168
+ - lib/ask/app_server/agent_adapter.rb
169
+ - lib/ask/app_server/cli.rb
170
+ - lib/ask/app_server/config.rb
171
+ - lib/ask/app_server/event_translator.rb
172
+ - lib/ask/app_server/permission_handler.rb
173
+ - lib/ask/app_server/server.rb
174
+ - lib/ask/app_server/session_manager.rb
175
+ - lib/ask/app_server/session_store.rb
176
+ - lib/ask/app_server/version.rb
177
+ homepage: https://github.com/ask-rb/ask-app-server
178
+ licenses:
179
+ - MIT
180
+ metadata:
181
+ homepage_uri: https://github.com/ask-rb/ask-app-server
182
+ source_code_uri: https://github.com/ask-rb/ask-app-server
183
+ changelog_uri: https://github.com/ask-rb/ask-app-server/blob/master/CHANGELOG.md
184
+ rdoc_options: []
185
+ require_paths:
186
+ - lib
187
+ required_ruby_version: !ruby/object:Gem::Requirement
188
+ requirements:
189
+ - - ">="
190
+ - !ruby/object:Gem::Version
191
+ version: '3.2'
192
+ required_rubygems_version: !ruby/object:Gem::Requirement
193
+ requirements:
194
+ - - ">="
195
+ - !ruby/object:Gem::Version
196
+ version: '0'
197
+ requirements: []
198
+ rubygems_version: 4.0.3
199
+ specification_version: 4
200
+ summary: JSON-RPC app-server for ask-rb agents
201
+ test_files: []