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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 77f123a7ad41bee740dc6678faa847ee174fc688ac09cab76b43c3ea2037da71
4
+ data.tar.gz: 1afcf87beb1af260dfc6042305e10d00d06b546c2acb464fe89e757732fccc31
5
+ SHA512:
6
+ metadata.gz: c7179daf83e380074cc04d7584dbaf4043d2c7cfff3df6e49c23a6fdc68bf34fc2b421b49866056a162702eb66d6054ecd437d63365f34dfad5a1cbb0968ce78
7
+ data.tar.gz: ad552bb79034f61d6b06a519cb041f711db6eb0f34c67b8e069cfc5c973782dcb47ab21d226f5726f78ee58992b3dbdc65322fbe59e0585cb73346ccda227eb8
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-07-23
4
+
5
+ ### Added
6
+
7
+ - Initial release of `ask-app-server` — JSON-RPC/stdio app-server for ask-rb agents.
8
+ - **Protocol handler** — implements the ZCode/Codex app-server JSON-RPC protocol over stdio.
9
+ - **Agent adapter** — wraps `Ask::Agent::Session` behind the app-server protocol.
10
+ - **Event translator** — converts ask-agent event types to app-server protocol event types.
11
+ - **Session manager** — create, list, resume, subscribe, and poll sessions.
12
+ - **CLI binary** — `ask-app-server` command (stdio mode).
13
+ - **Mid-execution injection** — abort running turns and send new messages.
14
+ - **Subscription streaming** — push `session/event` notifications to subscribed clients.
15
+ - **Event polling** — retrieve events by sequence number via `session/events`.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kaka Ruto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # ask-app-server
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/ask-app-server.svg)](https://badge.fury.io/rb/ask-app-server)
4
+
5
+ **JSON-RPC/stdio app-server for ask-rb agents.** Exposes `Ask::Agent::Session` behind the standard app-server protocol, the same protocol spoken by ZCode and Codex app-servers.
6
+
7
+ ## What is this?
8
+
9
+ `ask-app-server` turns an ask-rb agent into a **programmable service** that speaks JSON-RPC over stdio. Any client that can speak the app-server protocol can drive your agent:
10
+
11
+ - **Telegram bots**: the `zcode-telegram-bot` connects to ask-app-server instead of ZCode
12
+ - **AI SDK providers**: the Vercel AI SDK provider works with ask-app-server unchanged
13
+ - **IDE extensions**: VS Code, Cursor, and JetBrains extensions connect over stdio/socket
14
+ - **Headless automation**: CI/CD pipelines, batch processing, scriptable agent tasks
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ gem install ask-app-server
20
+ ```
21
+
22
+ Or add to your Gemfile:
23
+
24
+ ```ruby
25
+ gem "ask-app-server"
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ # Start the server (reads JSON-RPC from stdin, writes to stdout)
32
+ ask-app-server
33
+ ```
34
+
35
+ From another process, send JSON-RPC requests:
36
+
37
+ ```json
38
+ {"id":1, "method":"session/create", "params":{"workspace":{"workspacePath":"."}}}
39
+ {"id":2, "method":"session/send", "params":{"sessionId":"...", "content":"List files in this directory"}}
40
+ ```
41
+
42
+ ## Protocol
43
+
44
+ ### Methods
45
+
46
+ | Method | Description |
47
+ |---|---|
48
+ | `initialize` | Handshake, returns server capabilities |
49
+ | `session/create` | Create a new agent session |
50
+ | `session/list` | List active sessions |
51
+ | `session/resume` | Resume an existing session |
52
+ | `session/subscribe` | Subscribe to streaming events |
53
+ | `session/send` | Send a message to a session |
54
+ | `session/events` | Poll for events after a sequence number |
55
+ | `session/abort` | Abort the current turn |
56
+ | `workspace/readState` | Read model and workspace settings |
57
+
58
+ ### Events (server → client notifications)
59
+
60
+ | Event | When |
61
+ |---|---|
62
+ | `turn.started` | A new turn begins processing |
63
+ | `model.streaming` | Text delta from the model |
64
+ | `tool.updated` | Tool execution started/updated/completed/failed |
65
+ | `turn.completed` | Turn finished successfully |
66
+ | `turn.failed` | Turn ended with an error |
67
+
68
+ Event payloads are delivered as `session/event` notifications on subscribed sessions. The server also sends `interaction/requestPermission` when a blocked tool needs approval and `interaction/requestUserInput` when it needs input from the user.
69
+
70
+ ## Clients
71
+
72
+ This server is a drop-in replacement for `zcode app-server`. The following clients work without changes:
73
+
74
+ - [ask-coding-providers](https://github.com/ask-rb/ask-coding-providers): ZCode adapter (set `ZCODE_CLI_PATH` to the `ask-app-server` binary)
75
+ - [zcode-telegram-bot](https://github.com/ask-rb/zcode-telegram-bot): Python Telegram bot
76
+ - [ai-sdk-provider-codex-app-server](https://github.com/pablof7z/ai-sdk-provider-codex-app-server): Vercel AI SDK provider
77
+
78
+ ## Configuration
79
+
80
+ Flags: `--version`, `--help`, `--config PATH`.
81
+
82
+ The config file is searched in order: `ASK_APP_SERVER_CONFIG` env var, then `./.ask-app-server.json`, then `~/.ask-app-server/config.json`.
83
+
84
+ Environment variables:
85
+
86
+ | Variable | Default | Description |
87
+ |---|---|---|
88
+ | `ASK_APP_SERVER_CONFIG` | auto-detected | Path to the config file |
89
+ | `ASK_APP_SERVER_MODEL` | `opencode_go/deepseek-v4-flash` | Model identifier (overrides config file) |
90
+ | `ASK_APP_SERVER_PERMISSIONS` | `on_request` | Permission mode (`on_request`, `never`) |
91
+ | `DEBUG` | unset | Set to `1` for debug logging |
92
+
93
+ ## Development
94
+
95
+ ```bash
96
+ git clone https://github.com/ask-rb/ask-app-server.git
97
+ cd ask-app-server
98
+ bundle install
99
+ bundle exec rake test
100
+ ```
101
+
102
+ ## Full documentation
103
+
104
+ The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs. The [Gem reference](https://ask-rb.github.io/ask-docs/reference/gems) (Agent Infrastructure section) covers ask-app-server. API reference: https://ask-rb.github.io/ask-docs/reference/api.
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/ask-app-server"
5
+
6
+ Ask::AppServer::CLI.run!
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module AppServer
5
+ # Wraps an Ask::Agent::Session and translates its events into the
6
+ # app-server protocol event format via EventTranslator.
7
+ #
8
+ # Each wrapper is associated with one session and maintains an
9
+ # EventTranslator that app-server clients poll or subscribe to.
10
+ class AgentAdapter
11
+ # The underlying ask-agent session.
12
+ attr_reader :session
13
+
14
+ # The event translator that accumulates protocol events.
15
+ attr_reader :translator
16
+
17
+ # The session ID (same as ask-agent session id).
18
+ attr_reader :session_id
19
+
20
+ # Whether a turn is currently in progress.
21
+ attr_reader :running
22
+
23
+ # When the session was created.
24
+ attr_reader :created_at
25
+
26
+ def initialize(model:, tools: nil, system_prompt: nil, agent_dir: nil, **session_opts)
27
+ @model = model
28
+ @system_prompt = system_prompt
29
+ @tools = resolve_tools(tools)
30
+ @session_opts = session_opts
31
+ @agent_dir = agent_dir
32
+ @session = nil
33
+ @translator = nil
34
+ @session_id = nil
35
+ @running = false
36
+ @running_mutex = Mutex.new
37
+ @run_thread = nil
38
+ @abort_requested = false
39
+ @created_at = Time.now
40
+ @logger = Logger.new($stdout, level: ENV["DEBUG"] ? Logger::DEBUG : Logger::WARN)
41
+ end
42
+
43
+ # Start a new ask-agent session.
44
+ # Returns the session ID.
45
+ def start_session
46
+ opts = @session_opts.dup
47
+
48
+ # Extract hooks from session opts and pass them to Session
49
+ hooks = opts.delete(:hooks) || {}
50
+
51
+ @session = Ask::Agent::Session.new(
52
+ model: @model,
53
+ tools: @tools,
54
+ system_prompt: @system_prompt,
55
+ agent_dir: @agent_dir,
56
+ hooks: hooks,
57
+ **opts
58
+ )
59
+ @session_id = @session.id
60
+ @translator = EventTranslator.new(@session_id)
61
+ @session.on_event { |event| handle_agent_event(event) }
62
+ @session_id
63
+ end
64
+
65
+ # Resume an existing session (re-attach event handler).
66
+ def resume(session)
67
+ @session = session
68
+ @session_id = session.id
69
+ @translator = EventTranslator.new(@session_id)
70
+ @session.on_event { |event| handle_agent_event(event) }
71
+ @session_id
72
+ end
73
+
74
+ # Send a message and start processing. Runs in a background thread.
75
+ # The caller should poll or subscribe to receive events.
76
+ def send_message(content)
77
+ raise "Session not started" unless @session
78
+ raise "Session already busy" if @running
79
+
80
+ @running_mutex.synchronize do
81
+ @abort_requested = false
82
+ @running = true
83
+ end
84
+
85
+ @run_thread = Thread.new do
86
+ begin
87
+ @session.run(content)
88
+ rescue => e
89
+ # Agent may have been aborted — that's fine
90
+ @logger.debug("Agent run error: #{e.message}") if ENV["DEBUG"]
91
+ ensure
92
+ @running_mutex.synchronize { @running = false }
93
+ end
94
+ end
95
+
96
+ true
97
+ end
98
+
99
+ # Request abort of the current turn.
100
+ def abort_turn!
101
+ @abort_requested = true
102
+ @session&.abort if @session
103
+ end
104
+
105
+ # Wait for the current turn to complete (with timeout).
106
+ # Returns true if completed, false if timed out.
107
+ def wait_for_turn(timeout: 600)
108
+ thread = @run_thread
109
+ return true unless thread
110
+
111
+ thread.join(timeout)
112
+ !thread.alive?
113
+ end
114
+
115
+ # Whether this session is idle (no turn running).
116
+ def idle?
117
+ !@running
118
+ end
119
+
120
+ # Inject a message into a running session (mid-execution).
121
+ # Aborts the current turn; the message will be processed when
122
+ # the next turn starts.
123
+ # Note: ask-agent doesn't natively support mid-execution injection.
124
+ # We abort and queue the message for the next run.
125
+ def inject_message(content)
126
+ if @running
127
+ abort_turn!
128
+ # The caller should wait for idle, then call send_message again
129
+ false
130
+ else
131
+ send_message(content)
132
+ true
133
+ end
134
+ end
135
+
136
+ # The accumulated streaming text from the current/ last turn.
137
+ def streaming_text
138
+ @translator&.instance_variable_get(:@streaming_text).to_s
139
+ end
140
+
141
+ # All events since last drain.
142
+ def pending_events
143
+ @translator&.pending_events || []
144
+ end
145
+
146
+ # Drain and return pending events.
147
+ def drain_events
148
+ @translator&.drain_events || []
149
+ end
150
+
151
+ # Last sequence number.
152
+ def last_seq
153
+ @translator&.last_seq || 0
154
+ end
155
+
156
+ # Events after a given sequence number.
157
+ def events_after(after_seq)
158
+ pending_events.select { |e| e[:seq] > after_seq }
159
+ end
160
+
161
+ private
162
+
163
+ def handle_agent_event(event)
164
+ translated = @translator.translate(event)
165
+ # Translated events are already stored in the translator's buffer.
166
+ # Nothing else to do here — clients poll or subscribe to get them.
167
+ end
168
+
169
+ def resolve_tools(tool_list)
170
+ return default_tools if tool_list.nil?
171
+
172
+ tool_list.map do |t|
173
+ case t
174
+ when Class then t.new
175
+ when String then resolve_tool_by_name(t)
176
+ else t
177
+ end
178
+ end
179
+ end
180
+
181
+ def resolve_tool_by_name(name)
182
+ case name.downcase
183
+ when "bash" then Ask::Tools::Bash.new
184
+ when "read" then Ask::Tools::Read.new
185
+ when "write" then Ask::Tools::Write.new
186
+ when "edit" then Ask::Tools::Edit.new
187
+ when "glob" then Ask::Tools::Glob.new
188
+ when "grep" then Ask::Tools::Grep.new
189
+ when "code" then Ask::Tools::Code.new
190
+ else raise ArgumentError, "Unknown tool: #{name}"
191
+ end
192
+ end
193
+
194
+ def default_tools
195
+ [
196
+ Ask::Tools::Bash.new,
197
+ Ask::Tools::Read.new,
198
+ Ask::Tools::Write.new,
199
+ Ask::Tools::Edit.new,
200
+ Ask::Tools::Glob.new,
201
+ Ask::Tools::Grep.new
202
+ ]
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Ask
6
+ module AppServer
7
+ # CLI for the ask-app-server command.
8
+ #
9
+ # Usage:
10
+ # ask-app-server # Start in stdio mode (default)
11
+ # ask-app-server --version # Show version
12
+ # ask-app-server --help # Show help
13
+ # ask-app-server --config PATH # Use specific config file
14
+ #
15
+ # Environment variables:
16
+ # ASK_APP_SERVER_CONFIG - Path to config file (default: auto-detect)
17
+ # ASK_APP_SERVER_MODEL - Model to use (overrides config file)
18
+ # ASK_APP_SERVER_PERMISSIONS - Permission mode (overrides config file)
19
+ # DEBUG - Enable debug logging (1/0)
20
+ class CLI
21
+ def self.run!(args = ARGV)
22
+ new.run(args)
23
+ end
24
+
25
+ def run(args)
26
+ case args.first
27
+ when "--version", "-v"
28
+ puts "ask-app-server v#{Ask::AppServer::VERSION}"
29
+ return
30
+ when "--help", "-h"
31
+ show_help
32
+ return
33
+ end
34
+
35
+ # Parse --config from args
36
+ config_path = nil
37
+ remaining_args = []
38
+ args.each_with_index do |arg, i|
39
+ if arg == "--config" && i + 1 < args.length
40
+ config_path = args[i + 1]
41
+ elsif !arg.start_with?("--config")
42
+ remaining_args << arg
43
+ end
44
+ end
45
+
46
+ # Load configuration
47
+ config = Config.new(config_path: config_path)
48
+
49
+ # Register custom models from config into the model catalog
50
+ config.register_models!
51
+
52
+ $stdout.sync = true
53
+ $stderr.sync = true
54
+
55
+ if config.debug?
56
+ $stderr.puts "[ask-app-server] Starting v#{Ask::AppServer::VERSION}"
57
+ $stderr.puts "[ask-app-server] Config: #{JSON.pretty_generate(config.to_h)}"
58
+ end
59
+
60
+ # Build the session manager with config values
61
+ store = build_state_store(config)
62
+ session_manager = SessionManager.new(
63
+ store: store,
64
+ permission_mode: config.permission_mode,
65
+ blocked_tools: config.blocked_tools,
66
+ permission_timeout: config.permission_timeout
67
+ )
68
+
69
+ # Start the server
70
+ server = Server.new(session_manager: session_manager)
71
+
72
+ # Wire the server as the protocol sender for permission requests.
73
+ # Every time a new session is created with a PermissionHandler,
74
+ # the server registers its outgoing request callback.
75
+ session_manager.on_new_permission_handler do |handler|
76
+ server.register_permission_handler(handler)
77
+ end
78
+
79
+ begin
80
+ server.start
81
+ rescue Interrupt
82
+ $stderr.puts "\n[ask-app-server] Shutting down..." if config.debug?
83
+ server.stop
84
+ end
85
+ end
86
+
87
+ private
88
+
89
+ def build_state_store(config)
90
+ # Default path: ~/.ask-app-server/state.db
91
+ sqlite_path = config.state_sqlite_path || File.expand_path("~/.ask-app-server/state.db")
92
+
93
+ begin
94
+ require "sqlite3"
95
+ require "ask/state/providers/sqlite"
96
+ rescue LoadError
97
+ $stderr.puts "[ask-app-server] sqlite3 gem not available, using in-memory state store." if config.debug?
98
+ return nil
99
+ end
100
+
101
+ dir = File.dirname(File.expand_path(sqlite_path))
102
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
103
+
104
+ state = Ask::State::Providers::SQLite.new(path: sqlite_path)
105
+ $stderr.puts "[ask-app-server] State store: #{sqlite_path}" if config.debug?
106
+ Ask::AppServer::SessionStore.new(state: state)
107
+ rescue => e
108
+ $stderr.puts "[ask-app-server] Warning: Could not initialize SQLite state: #{e.message}"
109
+ $stderr.puts "[ask-app-server] Falling back to in-memory state store."
110
+ nil
111
+ end
112
+
113
+ def show_help
114
+ puts <<~HELP
115
+ ask-app-server v#{Ask::AppServer::VERSION}
116
+
117
+ JSON-RPC/stdio app-server for ask-rb agents.
118
+ Drop-in compatible with the ZCode/Codex app-server protocol.
119
+
120
+ USAGE:
121
+ ask-app-server Start in stdio mode
122
+
123
+ OPTIONS:
124
+ --version, -v Show version
125
+ --help, -h Show this help message
126
+ --config PATH Config file path (default: auto-detect)
127
+
128
+ CONFIG FILE (JSON):
129
+ Search order:
130
+ 1. ASK_APP_SERVER_CONFIG env
131
+ 2. ./.ask-app-server.json
132
+ 3. ~/.ask-app-server/config.json
133
+
134
+ Example ~/.ask-app-server/config.json:
135
+ {
136
+ "model": "claude-sonnet-4",
137
+ "tools": ["bash", "read", "write", "edit", "glob", "grep"],
138
+ "permissions": {
139
+ "mode": "on_request",
140
+ "blocked_tools": ["write", "edit", "bash", "destroy"],
141
+ "timeout": 300
142
+ },
143
+ "system_prompt": "You are a coding assistant...",
144
+ "session": { "timeout": 600 }
145
+ }
146
+
147
+ ENVIRONMENT:
148
+ ASK_APP_SERVER_CONFIG Config file path
149
+ ASK_APP_SERVER_MODEL Model identifier (overrides config)
150
+ ASK_APP_SERVER_PERMISSIONS Permission mode (overrides config)
151
+ DEBUG Set to 1 for debug logging
152
+
153
+ PROTOCOL:
154
+ Speaks the standard app-server JSON-RPC protocol over stdio.
155
+ Compatible clients:
156
+ - ask-coding-providers ZCode adapter
157
+ - ai-sdk-provider-codex-app-server (Vercel AI SDK)
158
+ - zcode-telegram-bot (Python Telegram bot)
159
+
160
+ EXAMPLE:
161
+ # Start with defaults
162
+ ask-app-server
163
+
164
+ # Start with a specific config
165
+ ask-app-server --config /path/to/config.json
166
+
167
+ # Send a ping from another shell:
168
+ echo '{"id":1,"method":"ping"}' | ask-app-server
169
+ HELP
170
+ end
171
+ end
172
+ end
173
+ end