ask-app-server 0.1.2 → 0.4.2
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 +4 -4
- data/CHANGELOG.md +99 -0
- data/README.md +60 -23
- data/lib/ask/app_server/agent_adapter.rb +239 -57
- data/lib/ask/app_server/cli.rb +31 -16
- data/lib/ask/app_server/connection.rb +73 -0
- data/lib/ask/app_server/event_translator.rb +173 -117
- data/lib/ask/app_server/herdr_reporter.rb +201 -0
- data/lib/ask/app_server/server.rb +333 -185
- data/lib/ask/app_server/session_manager.rb +166 -80
- data/lib/ask/app_server/socket_server.rb +116 -0
- data/lib/ask/app_server/version.rb +1 -1
- data/lib/ask/app_server.rb +2 -0
- data/lib/ask-app-server.rb +4 -0
- metadata +22 -5
data/lib/ask/app_server/cli.rb
CHANGED
|
@@ -8,14 +8,16 @@ module Ask
|
|
|
8
8
|
#
|
|
9
9
|
# Usage:
|
|
10
10
|
# ask-app-server # Start in stdio mode (default)
|
|
11
|
-
# ask-app-server --
|
|
12
|
-
# ask-app-server --
|
|
13
|
-
# ask-app-server --
|
|
11
|
+
# ask-app-server --socket PATH # Also listen on a unix socket (multi-client)
|
|
12
|
+
# ask-app-server --version # Show version
|
|
13
|
+
# ask-app-server --help # Show help
|
|
14
|
+
# ask-app-server --config PATH # Use specific config file
|
|
14
15
|
#
|
|
15
16
|
# Environment variables:
|
|
16
17
|
# ASK_APP_SERVER_CONFIG - Path to config file (default: auto-detect)
|
|
17
18
|
# ASK_APP_SERVER_MODEL - Model to use (overrides config file)
|
|
18
19
|
# ASK_APP_SERVER_PERMISSIONS - Permission mode (overrides config file)
|
|
20
|
+
# ASK_APP_SERVER_SOCKET - Unix socket path (same as --socket)
|
|
19
21
|
# DEBUG - Enable debug logging (1/0)
|
|
20
22
|
class CLI
|
|
21
23
|
def self.run!(args = ARGV)
|
|
@@ -32,14 +34,14 @@ module Ask
|
|
|
32
34
|
return
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
# Parse --config from args
|
|
37
|
+
# Parse --config and --socket from args
|
|
36
38
|
config_path = nil
|
|
37
|
-
|
|
39
|
+
socket_path = nil
|
|
38
40
|
args.each_with_index do |arg, i|
|
|
39
41
|
if arg == "--config" && i + 1 < args.length
|
|
40
42
|
config_path = args[i + 1]
|
|
41
|
-
elsif
|
|
42
|
-
|
|
43
|
+
elsif arg == "--socket" && i + 1 < args.length
|
|
44
|
+
socket_path = args[i + 1]
|
|
43
45
|
end
|
|
44
46
|
end
|
|
45
47
|
|
|
@@ -66,21 +68,34 @@ module Ask
|
|
|
66
68
|
permission_timeout: config.permission_timeout
|
|
67
69
|
)
|
|
68
70
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
#
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
# Pane integration: when running inside a terminal workspace,
|
|
72
|
+
# keep its agent-state sidebar accurate from first-party host
|
|
73
|
+
# events.
|
|
74
|
+
herdr_reporter = HerdrReporter.attach(session_manager)
|
|
75
|
+
|
|
76
|
+
# Optional unix-socket transport for multi-client attach (runs
|
|
77
|
+
# alongside the stdio transport, sharing the session manager).
|
|
78
|
+
socket_path ||= ENV["ASK_APP_SERVER_SOCKET"]
|
|
79
|
+
socket_server = nil
|
|
80
|
+
if socket_path
|
|
81
|
+
socket_server = SocketServer.new(session_manager: session_manager, socket_path: socket_path)
|
|
82
|
+
socket_server.start
|
|
83
|
+
$stderr.puts "socket: #{socket_server.socket_path}"
|
|
77
84
|
end
|
|
78
85
|
|
|
86
|
+
# Start the server (stdio transport, blocks)
|
|
87
|
+
server = Server.new(session_manager: session_manager)
|
|
88
|
+
|
|
79
89
|
begin
|
|
80
90
|
server.start
|
|
81
91
|
rescue Interrupt
|
|
82
92
|
$stderr.puts "\n[ask-app-server] Shutting down..." if config.debug?
|
|
93
|
+
ensure
|
|
94
|
+
# Clean up on Ctrl-C AND on normal exit (stdin EOF): the socket
|
|
95
|
+
# file must not outlive the host.
|
|
83
96
|
server.stop
|
|
97
|
+
socket_server&.stop
|
|
98
|
+
herdr_reporter&.close
|
|
84
99
|
end
|
|
85
100
|
end
|
|
86
101
|
|
|
@@ -115,7 +130,7 @@ module Ask
|
|
|
115
130
|
ask-app-server v#{Ask::AppServer::VERSION}
|
|
116
131
|
|
|
117
132
|
JSON-RPC/stdio app-server for ask-rb agents.
|
|
118
|
-
Drop-in compatible with the
|
|
133
|
+
Drop-in compatible with the app-server protocol.
|
|
119
134
|
|
|
120
135
|
USAGE:
|
|
121
136
|
ask-app-server Start in stdio mode
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module AppServer
|
|
7
|
+
# A client connection to the session host: an input/output pair (stdio
|
|
8
|
+
# or a unix socket) plus the connection's per-session event delivery
|
|
9
|
+
# cursors.
|
|
10
|
+
#
|
|
11
|
+
# Delivery is cursor-based: the connection tracks the last delivered
|
|
12
|
+
# seq for each subscribed session, so any number of clients can attach
|
|
13
|
+
# to the same sessions and each receives exactly the events after its
|
|
14
|
+
# own cursor. Subscribe with afterSeq to replay; clients dedup by seq.
|
|
15
|
+
class Connection
|
|
16
|
+
# session_id => last delivered seq
|
|
17
|
+
attr_reader :subscriptions
|
|
18
|
+
|
|
19
|
+
# @param input [IO] source of NDJSON lines (responds to gets)
|
|
20
|
+
# @param output [IO] sink for NDJSON responses/notifications
|
|
21
|
+
def initialize(input, output)
|
|
22
|
+
@input = input
|
|
23
|
+
@output = output
|
|
24
|
+
@subscriptions = {}
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Read one line from the input, or nil on EOF/disconnect.
|
|
29
|
+
def read_line
|
|
30
|
+
@input.gets
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Write one JSON-RPC message (Hash) to the output.
|
|
34
|
+
def write(msg)
|
|
35
|
+
@output.puts(JSON.generate(msg))
|
|
36
|
+
@output.flush
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Whether the connection is subscribed to a session.
|
|
40
|
+
def subscribed?(session_id)
|
|
41
|
+
@mutex.synchronize { @subscriptions.key?(session_id) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Subscribe to a session, starting delivery after after_seq.
|
|
45
|
+
def subscribe(session_id, after_seq: 0)
|
|
46
|
+
@mutex.synchronize { @subscriptions[session_id] = after_seq.to_i }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Unsubscribe from a session.
|
|
50
|
+
def unsubscribe(session_id)
|
|
51
|
+
@mutex.synchronize { @subscriptions.delete(session_id) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# The last delivered seq for a session (0 = replay from the start).
|
|
55
|
+
def cursor(session_id)
|
|
56
|
+
@mutex.synchronize { @subscriptions[session_id] || 0 }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Advance the delivery cursor for a session (only while subscribed).
|
|
60
|
+
def advance(session_id, seq)
|
|
61
|
+
@mutex.synchronize do
|
|
62
|
+
@subscriptions[session_id] = seq if @subscriptions.key?(session_id)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Drop all subscriptions. Does not close the underlying I/O — the
|
|
67
|
+
# owner (the server) manages IO lifecycle.
|
|
68
|
+
def close
|
|
69
|
+
@mutex.synchronize { @subscriptions.clear }
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -4,89 +4,124 @@ require "securerandom"
|
|
|
4
4
|
|
|
5
5
|
module Ask
|
|
6
6
|
module AppServer
|
|
7
|
-
# Translates ask-agent
|
|
7
|
+
# Translates ask-agent runtime events into canonical
|
|
8
|
+
# Ask::SessionProtocol events (the contract between the host and every
|
|
9
|
+
# client). Every emitted event is validated against the protocol
|
|
10
|
+
# registry, so a malformed translation fails fast at the boundary.
|
|
8
11
|
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# This class maps between the two models, emitting app-server protocol events
|
|
14
|
-
# that clients such as the Python Telegram bot and Vercel AI SDK expect.
|
|
12
|
+
# The translator owns the per-session event buffer and sequence
|
|
13
|
+
# numbers: clients poll (`session/events` after seq N) or subscribe
|
|
14
|
+
# (the server pushes drained events as `session/event` notifications).
|
|
15
15
|
class EventTranslator
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
# Events retained per session for replay/polling. Cursor-based
|
|
17
|
+
# delivery means the log is append-only; clients with cursors older
|
|
18
|
+
# than the cap miss the dropped events (durable log is a host
|
|
19
|
+
# storage concern).
|
|
20
|
+
MAX_EVENTS = 2000
|
|
21
|
+
|
|
22
|
+
# Optional observer called with every emitted canonical Event.
|
|
23
|
+
# This is the single choke point for all session events (translations
|
|
24
|
+
# plus approval/plan/session-lifecycle emissions), so host-side
|
|
25
|
+
# side effects (e.g. the pane reporter) hook here.
|
|
26
|
+
attr_accessor :on_event
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
22
29
|
@events = []
|
|
23
|
-
@
|
|
30
|
+
@seq = 0
|
|
31
|
+
@turn_id = nil
|
|
24
32
|
@turn_active = false
|
|
25
|
-
@
|
|
33
|
+
@streaming_text = +""
|
|
34
|
+
@plan_interaction_id = nil
|
|
26
35
|
end
|
|
27
36
|
|
|
28
|
-
# Translate
|
|
29
|
-
# Returns an array of
|
|
37
|
+
# Translate one ask-agent event into zero or more canonical events.
|
|
38
|
+
# Returns an array of Ask::SessionProtocol::Events::Event (may be
|
|
39
|
+
# empty); events are buffered for delivery.
|
|
30
40
|
def translate(agent_event)
|
|
31
41
|
case agent_event
|
|
32
42
|
when Ask::Agent::Events::TurnStart
|
|
33
|
-
|
|
43
|
+
turn_started
|
|
34
44
|
when Ask::Agent::Events::TextDelta
|
|
35
|
-
|
|
45
|
+
text_delta(agent_event)
|
|
46
|
+
when Ask::Agent::Events::ThinkingDelta
|
|
47
|
+
thinking_delta(agent_event)
|
|
36
48
|
when Ask::Agent::Events::ToolCallDelta
|
|
37
|
-
#
|
|
38
|
-
# until execution actually starts.
|
|
49
|
+
# Informational; execution events carry the tool lifecycle.
|
|
39
50
|
[]
|
|
40
51
|
when Ask::Agent::Events::ToolExecutionStart
|
|
41
|
-
|
|
52
|
+
tool_start(agent_event)
|
|
42
53
|
when Ask::Agent::Events::ToolExecutionUpdate
|
|
43
|
-
|
|
54
|
+
tool_update(agent_event)
|
|
44
55
|
when Ask::Agent::Events::ToolExecutionEnd
|
|
45
|
-
|
|
46
|
-
when Ask::Agent::Events::
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
when Ask::Agent::Events::
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
when Ask::Agent::Events::ReflectionStart
|
|
55
|
-
# Reflection is an internal detail — skip
|
|
56
|
-
@in_reflection = true
|
|
57
|
-
[]
|
|
58
|
-
when Ask::Agent::Events::ReflectionDelta
|
|
59
|
-
# Treat reflection text as regular model output
|
|
60
|
-
translate_text_delta(agent_event)
|
|
61
|
-
when Ask::Agent::Events::ReflectionEnd
|
|
62
|
-
@in_reflection = false
|
|
63
|
-
[]
|
|
64
|
-
when Ask::Agent::Events::CompactionStart
|
|
65
|
-
[]
|
|
66
|
-
when Ask::Agent::Events::CompactionEnd
|
|
67
|
-
[]
|
|
68
|
-
when Ask::Agent::Events::Error
|
|
69
|
-
translate_error(agent_event)
|
|
70
|
-
when Ask::Agent::Events::SessionEnd
|
|
71
|
-
translate_session_end(agent_event)
|
|
56
|
+
tool_end(agent_event)
|
|
57
|
+
when Ask::Agent::Events::TodoUpdated
|
|
58
|
+
todos_updated(agent_event)
|
|
59
|
+
when Ask::Agent::Events::PlanProposed
|
|
60
|
+
plan_proposed(agent_event)
|
|
61
|
+
when Ask::Agent::Events::PlanApproved
|
|
62
|
+
plan_approved(agent_event)
|
|
63
|
+
when Ask::Agent::Events::PlanRejected
|
|
64
|
+
plan_rejected(agent_event)
|
|
72
65
|
when Ask::Agent::Events::MaxTurnsExceeded
|
|
73
|
-
|
|
66
|
+
turn_failed("Max turns exceeded (#{agent_event.max_turns})")
|
|
74
67
|
when Ask::Agent::Events::LoopDetected
|
|
75
|
-
|
|
68
|
+
turn_failed("Loop detected on tool: #{agent_event.tool_name}")
|
|
69
|
+
when Ask::Agent::Events::Error
|
|
70
|
+
error(agent_event)
|
|
71
|
+
when Ask::Agent::Events::SessionEnd
|
|
72
|
+
session_end(agent_event)
|
|
76
73
|
else
|
|
74
|
+
# TurnEnd, MessageStart/End, Reflection*, Compaction*,
|
|
75
|
+
# SessionRolledBack/Forked, Evaluation*, MetaAgentAnalysis —
|
|
76
|
+
# internal detail, no canonical representation.
|
|
77
77
|
[]
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
#
|
|
81
|
+
# ── Queue/plan-driven emission (not ask-agent events) ──────────────
|
|
82
|
+
|
|
83
|
+
# An approval action was queued: emit approval.required.
|
|
84
|
+
#
|
|
85
|
+
# @param action [Ask::Agent::ApprovalQueue::Action]
|
|
86
|
+
def approval_required(action)
|
|
87
|
+
payload = { "toolName" => action.tool_name.to_s }
|
|
88
|
+
payload["args"] = action.args if action.args
|
|
89
|
+
payload["message"] = action.message if action.message
|
|
90
|
+
payload["autoApprovable"] = action.auto_approvable unless action.auto_approvable.nil?
|
|
91
|
+
emit("approval.required", payload.merge("id" => "act_#{action.id}"))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# An approval action changed status: emit approval.updated.
|
|
95
|
+
#
|
|
96
|
+
# @param action [Ask::Agent::ApprovalQueue::Action]
|
|
97
|
+
def approval_updated(action)
|
|
98
|
+
status = action.status.to_s
|
|
99
|
+
return unless %w[approved rejected].include?(status)
|
|
100
|
+
|
|
101
|
+
emit("approval.updated", { "id" => "act_#{action.id}", "status" => status })
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# A session was created: emit session.created.
|
|
105
|
+
def session_created(session_id)
|
|
106
|
+
emit("session.created", { "sessionId" => session_id })
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# A session ended: emit session.ended.
|
|
110
|
+
def session_ended(session_id, reason: "closed")
|
|
111
|
+
emit("session.ended", { "sessionId" => session_id, "reason" => reason })
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# ── Buffer access ──────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
# All events since the last drain.
|
|
82
117
|
def pending_events
|
|
83
118
|
@events
|
|
84
119
|
end
|
|
85
120
|
|
|
86
121
|
# Drain and return all pending events, clearing the buffer.
|
|
87
122
|
def drain_events
|
|
88
|
-
evs = @events
|
|
89
|
-
@events
|
|
123
|
+
evs = @events
|
|
124
|
+
@events = []
|
|
90
125
|
evs
|
|
91
126
|
end
|
|
92
127
|
|
|
@@ -95,99 +130,120 @@ module Ask
|
|
|
95
130
|
@seq
|
|
96
131
|
end
|
|
97
132
|
|
|
133
|
+
# Events after a given sequence number.
|
|
134
|
+
def events_after(after_seq)
|
|
135
|
+
@events.select { |e| e.seq > after_seq }
|
|
136
|
+
end
|
|
137
|
+
|
|
98
138
|
private
|
|
99
139
|
|
|
100
140
|
def next_seq
|
|
101
141
|
@seq += 1
|
|
102
|
-
@seq
|
|
103
142
|
end
|
|
104
143
|
|
|
105
|
-
def
|
|
144
|
+
def turn_started
|
|
106
145
|
@turn_id = SecureRandom.uuid
|
|
107
146
|
@turn_active = true
|
|
108
147
|
@streaming_text = +""
|
|
109
|
-
|
|
110
|
-
ev = build_event("turn.started", { turnId: @turn_id })
|
|
111
|
-
[ev]
|
|
148
|
+
emit("turn.started", { "turnId" => @turn_id })
|
|
112
149
|
end
|
|
113
150
|
|
|
114
|
-
def
|
|
151
|
+
def text_delta(event)
|
|
115
152
|
content = event.content.to_s
|
|
116
153
|
return [] if content.empty?
|
|
117
154
|
|
|
118
155
|
@streaming_text << content
|
|
156
|
+
emit("model.streaming", { "delta" => content })
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def thinking_delta(event)
|
|
160
|
+
content = event.content.to_s
|
|
161
|
+
return [] if content.empty?
|
|
162
|
+
|
|
163
|
+
emit("model.thinking", { "delta" => content })
|
|
164
|
+
end
|
|
119
165
|
|
|
120
|
-
|
|
121
|
-
|
|
166
|
+
def tool_start(event)
|
|
167
|
+
payload = {
|
|
168
|
+
"id" => event.id.to_s,
|
|
169
|
+
"name" => event.name.to_s,
|
|
170
|
+
"args" => event.arguments
|
|
171
|
+
}
|
|
172
|
+
emit("tool.use", payload)
|
|
122
173
|
end
|
|
123
174
|
|
|
124
|
-
def
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
175
|
+
def tool_update(event)
|
|
176
|
+
payload = {
|
|
177
|
+
"id" => event.id.to_s,
|
|
178
|
+
"name" => event.name.to_s,
|
|
179
|
+
"partial" => event.partial_result.to_s
|
|
180
|
+
}
|
|
181
|
+
emit("tool.delta", payload)
|
|
131
182
|
end
|
|
132
183
|
|
|
133
|
-
def
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
output
|
|
138
|
-
|
|
139
|
-
|
|
184
|
+
def tool_end(event)
|
|
185
|
+
payload = {
|
|
186
|
+
"id" => event.id.to_s,
|
|
187
|
+
"name" => event.name.to_s,
|
|
188
|
+
"output" => event.result,
|
|
189
|
+
"isError" => !!event.is_error,
|
|
190
|
+
"durationMs" => event.duration_ms
|
|
191
|
+
}
|
|
192
|
+
emit("tool.result", payload)
|
|
140
193
|
end
|
|
141
194
|
|
|
142
|
-
def
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
toolName: event.name,
|
|
146
|
-
kind: kind,
|
|
147
|
-
output: event.result.to_s,
|
|
148
|
-
durationMs: event.duration_ms
|
|
149
|
-
})
|
|
150
|
-
[ev]
|
|
195
|
+
def todos_updated(event)
|
|
196
|
+
todos = event.todos.map { |entry| entry.to_h.transform_keys(&:to_s) }
|
|
197
|
+
emit("todos.updated", { "todos" => todos })
|
|
151
198
|
end
|
|
152
199
|
|
|
153
|
-
def
|
|
154
|
-
@
|
|
155
|
-
|
|
200
|
+
def plan_proposed(event)
|
|
201
|
+
@plan_interaction_id = "plan_#{next_seq}"
|
|
202
|
+
emit("plan.proposed", { "id" => @plan_interaction_id, "plan" => event.plan.to_s })
|
|
203
|
+
end
|
|
156
204
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
inputTokens: event.input_tokens,
|
|
162
|
-
outputTokens: event.output_tokens,
|
|
163
|
-
cost: event.cost
|
|
164
|
-
})
|
|
165
|
-
[ev]
|
|
205
|
+
def plan_approved(event)
|
|
206
|
+
payload = { "plan" => event.plan.to_s }
|
|
207
|
+
payload["id"] = @plan_interaction_id if @plan_interaction_id
|
|
208
|
+
emit("plan.approved", payload)
|
|
166
209
|
end
|
|
167
210
|
|
|
168
|
-
def
|
|
169
|
-
|
|
170
|
-
|
|
211
|
+
def plan_rejected(event)
|
|
212
|
+
payload = { "plan" => event.plan.to_s }
|
|
213
|
+
payload["id"] = @plan_interaction_id if @plan_interaction_id
|
|
214
|
+
emit("plan.rejected", payload)
|
|
171
215
|
end
|
|
172
216
|
|
|
173
|
-
def
|
|
217
|
+
def session_end(event)
|
|
174
218
|
@turn_active = false
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
[
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
219
|
+
payload = {}
|
|
220
|
+
payload["turnId"] = @turn_id if @turn_id
|
|
221
|
+
payload["response"] = event.result.to_s
|
|
222
|
+
payload["inputTokens"] = event.input_tokens if event.input_tokens
|
|
223
|
+
payload["outputTokens"] = event.output_tokens if event.output_tokens
|
|
224
|
+
payload["cost"] = event.cost if event.cost
|
|
225
|
+
emit("turn.completed", payload)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def error(event)
|
|
229
|
+
payload = { "error" => event.error.to_s }
|
|
230
|
+
payload["recoverable"] = event.recoverable unless event.recoverable.nil?
|
|
231
|
+
emit("error", payload)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def turn_failed(message)
|
|
235
|
+
@turn_active = false
|
|
236
|
+
payload = { "error" => message.to_s }
|
|
237
|
+
payload["turnId"] = @turn_id if @turn_id
|
|
238
|
+
emit("turn.failed", payload)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def emit(type, payload)
|
|
242
|
+
event = Ask::SessionProtocol::Events.event(type: type, seq: next_seq, payload: payload)
|
|
189
243
|
@events << event
|
|
190
|
-
|
|
244
|
+
@events.shift if @events.size > MAX_EVENTS
|
|
245
|
+
on_event&.call(event)
|
|
246
|
+
[event]
|
|
191
247
|
end
|
|
192
248
|
end
|
|
193
249
|
end
|