silas 0.3.1 → 0.3.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 +26 -0
- data/app/helpers/silas/inbox/trace_helper.rb +11 -1
- data/app/models/silas/tool_invocation.rb +7 -1
- data/app/views/silas/inbox/invocations/_approval_card.html.erb +5 -2
- data/app/views/silas/inbox/sessions/index.html.erb +1 -1
- data/app/views/silas/inbox/sessions/show.html.erb +1 -1
- data/app/views/silas/inbox/turns/_header.html.erb +5 -2
- data/lib/silas/budget.rb +5 -3
- data/lib/silas/engine.rb +11 -0
- data/lib/silas/inbox.rb +15 -0
- data/lib/silas/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a8ece90334c6059bd396179bfae54b62ebd8abc483d72309da718dbb8e147845
|
|
4
|
+
data.tar.gz: '014359b0b35a36e7b34b796fbb9194f99c49bf39a1901b4b82cb7b94a10c0ea0'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 25e3610da0c9a62b6e9f2a0f956e01c989c62eec40bfdee6703b1bd9206821bb6b4a7988726671b57cd53949ae518239a76d76511cc260b3279db9a36b4f38c4
|
|
7
|
+
data.tar.gz: f891bf52671a8cd2c7fdc9041b036194939995dc4de7dee7bdd8f09cea986593bc526681d161d41add81b16cfe9257d75df7599e09a7fb85d5af52a9b117828d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.2
|
|
4
|
+
|
|
5
|
+
- **The `timeout` budget no longer counts time spent parked for approval.**
|
|
6
|
+
The wall clock restarts when an approval resumes a turn, because any
|
|
7
|
+
approval slower than `limits.timeout` previously made the approved resume
|
|
8
|
+
*instantly* re-park on "timeout" — pathological for a gate whose whole point
|
|
9
|
+
is waiting for a person (found live in the playground: a 3-minute approval
|
|
10
|
+
against a 120s timeout). Timeout now bounds **active** stretches — hung
|
|
11
|
+
providers, runaway loops; crash-rescue resumes keep the original clock (the
|
|
12
|
+
turn was genuinely live), and cost/token budgets stay cumulative because
|
|
13
|
+
they measure real spend.
|
|
14
|
+
- **Broadcast-rendered trace partials work in host apps.** The inbox's live
|
|
15
|
+
trace renders through Turbo's broadcast jobs — i.e. the HOST's default
|
|
16
|
+
renderer — where the engine-scoped `TraceHelper` and bare engine route
|
|
17
|
+
helpers didn't exist, so **every broadcast render raised and the live trace
|
|
18
|
+
silently never streamed in real host apps** (the gem's specs stubbed the
|
|
19
|
+
dispatch seam and never rendered). The helper is now registered host-wide,
|
|
20
|
+
partials build routes context-free via `silas_engine_path` (engine route
|
|
21
|
+
set + discovered mount point), and four host-renderer regression specs pin
|
|
22
|
+
the real path. `relative_time` is renamed `silas_relative_time` (it is now
|
|
23
|
+
host-visible, and the bare name is exactly what a host app would define).
|
|
24
|
+
Also new: `examples/playground` gets a customer-facing chat page that
|
|
25
|
+
renders and live-streams the engine's own trace partials with zero custom
|
|
26
|
+
streaming code, plus a scripted keyless demo mode (`bin/setup && bin/dev`
|
|
27
|
+
with no API key).
|
|
28
|
+
|
|
3
29
|
## 0.3.1
|
|
4
30
|
|
|
5
31
|
- **Fixed: only the first scenario in a `silas:eval` run was really tested.**
|
|
@@ -37,11 +37,21 @@ module Silas
|
|
|
37
37
|
end
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
# Prefixed: this helper is registered host-wide (broadcast renders need
|
|
41
|
+
# it), and "relative_time" is exactly the name a host app would define.
|
|
42
|
+
def silas_relative_time(time)
|
|
41
43
|
return "" unless time
|
|
42
44
|
|
|
43
45
|
"#{time_ago_in_words(time)} ago"
|
|
44
46
|
end
|
|
47
|
+
|
|
48
|
+
# Engine paths that resolve in EVERY render context. The mounted proxy
|
|
49
|
+
# (`silas.`) leans on the rendering scope's url_options, which Turbo's
|
|
50
|
+
# bare broadcast renderer doesn't have — so broadcast-rendered partials
|
|
51
|
+
# build paths from the engine's own route set + the discovered mount.
|
|
52
|
+
def silas_engine_path(helper, *args)
|
|
53
|
+
Silas::Engine.routes.url_helpers.public_send(helper, *args, script_name: Silas::Inbox.mount_path)
|
|
54
|
+
end
|
|
45
55
|
end
|
|
46
56
|
end
|
|
47
57
|
end
|
|
@@ -84,7 +84,13 @@ module Silas
|
|
|
84
84
|
return if turn.reload.canceled? || turn.failed? # settled turns never zombie-resume
|
|
85
85
|
return if turn.tool_invocations.where(approval_state: "required").exists?
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
# Restart the wall clock: `limits.timeout` bounds ACTIVE stretches (hung
|
|
88
|
+
# providers, runaway loops), not human deliberation. Without this, any
|
|
89
|
+
# approval that took longer than the timeout made the approved resume
|
|
90
|
+
# instantly re-park on "timeout" — pathological for a gate whose whole
|
|
91
|
+
# point is waiting for a person. Cost/token budgets stay cumulative;
|
|
92
|
+
# they measure real spend.
|
|
93
|
+
turn.update!(status: "queued", started_at: Time.current)
|
|
88
94
|
AgentLoopJob.perform_later(turn.id)
|
|
89
95
|
end
|
|
90
96
|
end
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<div class="approval">
|
|
2
2
|
<h3>Approval needed — <%= invocation.tool_name %></h3>
|
|
3
3
|
<pre><%= pretty_args(invocation.arguments) %></pre>
|
|
4
|
-
|
|
4
|
+
<%# silas_engine_path, not bare helpers: this partial is broadcast-rendered
|
|
5
|
+
through the HOST's renderer, where engine route helpers don't exist and
|
|
6
|
+
the mounted proxy has no routing scope to lean on. %>
|
|
7
|
+
<%= form_with url: silas_engine_path(:approve_inbox_invocation_path, invocation), method: :post, class: "inline" do %>
|
|
5
8
|
<button class="btn approve">Approve</button>
|
|
6
9
|
<% end %>
|
|
7
|
-
<%= form_with url: decline_inbox_invocation_path
|
|
10
|
+
<%= form_with url: silas_engine_path(:decline_inbox_invocation_path, invocation), method: :post, class: "decline-form" do %>
|
|
8
11
|
<textarea name="reason" rows="2" placeholder="Reason (optional — sent back to the agent as the tool result)"></textarea>
|
|
9
12
|
<button class="btn decline">Decline</button>
|
|
10
13
|
<% end %>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<span class="name"><%= @session.agent_name %></span>
|
|
12
12
|
<span class="muted">#<%= @session.id %></span>
|
|
13
13
|
</div>
|
|
14
|
-
<div class="muted"><%= @session.channel.presence || "direct" %> · started <%=
|
|
14
|
+
<div class="muted"><%= @session.channel.presence || "direct" %> · started <%= silas_relative_time(@session.created_at) %></div>
|
|
15
15
|
<div id="silas-session-<%= @session.id %>-cost"><%= render "silas/inbox/sessions/cost", session: @session %></div>
|
|
16
16
|
</div>
|
|
17
17
|
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
<span class="turn-input"><%= truncate(turn.input, length: 90) %></span>
|
|
3
3
|
<%= status_pill(turn.status) %>
|
|
4
4
|
<% if turn.active? && !turn.cancel_requested_at %>
|
|
5
|
-
|
|
5
|
+
<%# silas_engine_path — this partial is broadcast-rendered through the
|
|
6
|
+
host renderer on every turn status change; bare engine helpers (and
|
|
7
|
+
the scope-dependent mounted proxy) 500 there. %>
|
|
8
|
+
<%= form_with url: silas_engine_path(:cancel_inbox_turn_path, turn), method: :post, class: "inline" do %>
|
|
6
9
|
<button class="btn-cancel" title="Honored at the next step boundary">Cancel</button>
|
|
7
10
|
<% end %>
|
|
8
11
|
<% elsif turn.cancel_requested_at && turn.active? %>
|
|
@@ -14,7 +17,7 @@
|
|
|
14
17
|
<h3>Budget reached — <%= turn.failure_reason %></h3>
|
|
15
18
|
<div class="muted">Parked at zero compute. Raise the limit and the turn resumes
|
|
16
19
|
by replaying completed work from rows — no re-calls, no re-effects.</div>
|
|
17
|
-
<%= form_with url: raise_budget_inbox_turn_path
|
|
20
|
+
<%= form_with url: silas_engine_path(:raise_budget_inbox_turn_path, turn), method: :post, class: "topup-form" do %>
|
|
18
21
|
<input name="value" type="number" step="any" min="0" required class="topup-input"
|
|
19
22
|
placeholder="<%= turn.failure_reason == "max_cost" ? "new limit in dollars, e.g. 2.50" : "new limit, e.g. 200000" %>">
|
|
20
23
|
<button class="btn approve">Raise & resume</button>
|
data/lib/silas/budget.rb
CHANGED
|
@@ -7,9 +7,11 @@ module Silas
|
|
|
7
7
|
#
|
|
8
8
|
# Token/cost checks are deterministic (persisted step data). The timeout check
|
|
9
9
|
# reads the wall clock — benign non-determinism: a cap firing later on resume
|
|
10
|
-
# is correct (the turn genuinely ran too long across the crash).
|
|
11
|
-
#
|
|
12
|
-
#
|
|
10
|
+
# is correct (the turn genuinely ran too long across the crash). The clock
|
|
11
|
+
# RESTARTS when an approval resumes the turn (resume_turn! resets
|
|
12
|
+
# started_at): timeout bounds active stretches, never the hours a human
|
|
13
|
+
# spends deciding. Crash-rescue resumes keep the original clock — the turn
|
|
14
|
+
# was live the whole time.
|
|
13
15
|
module Budget
|
|
14
16
|
REASONS = %w[max_input_tokens max_cost timeout].freeze
|
|
15
17
|
|
data/lib/silas/engine.rb
CHANGED
|
@@ -43,6 +43,17 @@ module Silas
|
|
|
43
43
|
Silas::Inbox::DeltaBroadcaster.subscribe!
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# The trace partials render in TWO contexts: the inbox controllers (which
|
|
47
|
+
# declare this helper) and Turbo's broadcast jobs, which render through the
|
|
48
|
+
# HOST's default renderer — where an engine-scoped helper doesn't exist and
|
|
49
|
+
# every broadcast died silently inside Turbo's job. Register it host-wide
|
|
50
|
+
# so broadcast renders — and host apps embedding the trace partials in
|
|
51
|
+
# their own pages — resolve it. (Routes inside those partials go through
|
|
52
|
+
# TraceHelper#silas_engine_path, which needs no routing scope at all.)
|
|
53
|
+
initializer "silas.trace_helper" do
|
|
54
|
+
ActiveSupport.on_load(:action_controller_base) { helper Silas::Inbox::TraceHelper }
|
|
55
|
+
end
|
|
56
|
+
|
|
46
57
|
# Registry rebuilds on every code reload in development, once in production.
|
|
47
58
|
initializer "silas.registry" do |app|
|
|
48
59
|
next unless app.root.join("app/agent").exist? || app.root.join("app/agents").exist?
|
data/lib/silas/inbox.rb
CHANGED
|
@@ -19,5 +19,20 @@ module Silas
|
|
|
19
19
|
def stream_name(session_id)
|
|
20
20
|
"silas:inbox:session:#{session_id}"
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
# Where the host mounted the engine ("/silas" by the installer's
|
|
24
|
+
# convention), discovered from the app's route set. Used to build paths
|
|
25
|
+
# that work in EVERY render context — including Turbo's bare broadcast
|
|
26
|
+
# renderer, which has no routing scope for the mounted proxy to lean on.
|
|
27
|
+
def mount_path
|
|
28
|
+
@mount_path ||= begin
|
|
29
|
+
route = Rails.application.routes.routes.find do |r|
|
|
30
|
+
r.app.respond_to?(:app) && r.app.app == Silas::Engine
|
|
31
|
+
end
|
|
32
|
+
route&.path&.spec.to_s.sub(/\(.*\z/, "").presence || "/silas"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def reset_mount_path! = (@mount_path = nil) # specs remount
|
|
22
37
|
end
|
|
23
38
|
end
|
data/lib/silas/version.rb
CHANGED