envoy_ai 0.0.2 → 0.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72b0cf7f247e26cf0c6d1aa41f24b29a8e2fee0501476e83ebc0dab5df927a01
4
- data.tar.gz: a7d6213134ee171ac420047502d31d05fdd562f2c49fa718dad2ee4d0d86981e
3
+ metadata.gz: 78234041fb1cc9c8df4b16e0125c24069f29c224bbb4fbaf601ecd72e831715f
4
+ data.tar.gz: 040c720efeaa6677fc1ceb05b3d1f8dec26b0cdcc110d2c0567ee3e536785829
5
5
  SHA512:
6
- metadata.gz: 32eb9d905212b9a2a1e63f2aa21d15147a65500015a48e7e07b08760d137fc3fb80507534e875e158f854d8c1c3557412e86b64fd5478736dde09fe2d4bce856
7
- data.tar.gz: 8e95fe42202f1f457488b7ec2b6f4d7bda439e082ca29c057cafe2c5fbd660f840e99e339597abfe389cf4e29a30094a36dec3ea03c8a9d4dc53ce20e7a108fc
6
+ metadata.gz: b76061cf8adc16a4cc216e05568a3e918508b0cf7fc81afe349325fcf1281349272738a24b2c3855cec9a8d4af9527b0442d367a73436060b9e84b063b92fb7e
7
+ data.tar.gz: 5abcc9d99056f8ed6835e457270df6e0f6ff75cf70da8b22642136112799ab3163bbc80665acb58ed6d036a69ad0835600ecdd3043456f8a980a1c893b39bdd7
@@ -7,10 +7,17 @@
7
7
  --envoy-ink, --envoy-subtle, --envoy-line, --envoy-accent,
8
8
  --envoy-accent-ink, --envoy-warn, --envoy-radius. */
9
9
 
10
+ /* Fills a flex-column container (an embedded, fixed-height panel) so the log
11
+ below can scroll internally. Inert when the parent isn't a flex container —
12
+ e.g. the full-page console, which keeps its natural height and page-scrolls —
13
+ because flex item properties only apply inside a flex parent. The embedding
14
+ host is responsible for giving this element a bounded height (a flex column
15
+ with a definite height); the transcript then fills it and scrolls its log. */
10
16
  .envoy-chat {
11
17
  display: flex;
12
18
  flex-direction: column;
13
19
  min-height: 0;
20
+ flex: 1 1 auto;
14
21
  }
15
22
 
16
23
  .envoy-chat__log {
@@ -18,6 +25,26 @@
18
25
  min-height: 0;
19
26
  overflow-y: auto;
20
27
  overscroll-behavior: contain;
28
+ scrollbar-width: thin;
29
+ scrollbar-color: var(--envoy-subtle, #6b7280) transparent;
30
+ }
31
+ .envoy-chat__log::-webkit-scrollbar { width: 8px; }
32
+ .envoy-chat__log::-webkit-scrollbar-track { background: transparent; }
33
+ .envoy-chat__log::-webkit-scrollbar-thumb {
34
+ background: var(--envoy-subtle, #6b7280);
35
+ border-radius: 9999px;
36
+ }
37
+ .envoy-chat__log::-webkit-scrollbar-thumb:hover {
38
+ background: var(--envoy-ink, #111827);
39
+ }
40
+
41
+ /* Separate the transcript from the composer: in the flex column the log sits
42
+ flush against the input form, so the last message and the textarea run
43
+ together without a divider. */
44
+ .envoy-chat > form {
45
+ margin-top: 0.75rem;
46
+ padding-top: 0.75rem;
47
+ border-top: 1px solid var(--envoy-line, #e5e7eb);
21
48
  }
22
49
 
23
50
  /* ---------------------------------------------------------------------- */
@@ -37,8 +37,41 @@ module Envoy
37
37
  system_prompt.presence
38
38
  end
39
39
 
40
+ # Truncate an interrupted tail so the next turn starts from a valid state.
41
+ #
42
+ # acts_as_chat replays the whole message log to the provider every turn. If a
43
+ # turn died without a RubyLLM::Error (a deploy restart mid-turn, an OOM, a
44
+ # network kill), RubyLLM's own cleanup never ran and the log can end with a
45
+ # blank assistant row or an assistant tool_use that has no result — both of
46
+ # which the provider rejects, bricking every future turn. Drop everything
47
+ # after the last *completed* assistant turn (non-blank content, every tool_use
48
+ # answered); if there is no such checkpoint yet, drop everything but a leading
49
+ # system prefix. Returns the number of messages removed. A steady conversation
50
+ # already ending on a completed turn is a no-op.
51
+ #
52
+ # Safe because a completed assistant turn could only have been produced if the
53
+ # log before it already replayed cleanly — so nothing before the checkpoint
54
+ # can be malformed.
55
+ def heal_interrupted_tail!
56
+ ordered = messages.order(:id).to_a
57
+ checkpoint = ordered.rindex { |m| completed_assistant_turn?(m) }
58
+ stale = checkpoint ? ordered[(checkpoint + 1)..] : ordered.reject { |m| m.role == "system" }
59
+ return 0 if stale.empty?
60
+
61
+ # Newest first so a tool-result message is removed before the tool_call it
62
+ # points at (destroyed via the assistant's dependent: :destroy).
63
+ stale.reverse_each(&:destroy!)
64
+ stale.size
65
+ end
66
+
40
67
  private
41
68
 
69
+ def completed_assistant_turn?(message)
70
+ return false unless message.role == "assistant"
71
+ return false if message.content.blank?
72
+ message.tool_calls.all? { |tc| Envoy::Message.exists?(tool_call_id: tc.id) }
73
+ end
74
+
42
75
  def toolset_or_surface_present
43
76
  return if toolset_key.present? || surface_key.present?
44
77
  errors.add(:base, "toolset_key or surface_key must be present")
data/lib/envoy/runner.rb CHANGED
@@ -12,6 +12,11 @@ module Envoy
12
12
  end
13
13
 
14
14
  def call(content:, &on_event)
15
+ # Repair an interrupted tail before replaying: a prior turn killed mid-flight
16
+ # (deploy restart, OOM, timeout) can leave a blank assistant or dangling
17
+ # tool_use that the provider rejects on every subsequent turn. Heal first so
18
+ # one bad turn can't permanently brick the conversation.
19
+ @conversation.heal_interrupted_tail!
15
20
  tools = compiled_tools
16
21
  Envoy.llm(conversation: @conversation)
17
22
  .run(content: content, tools: tools, instructions: instructions, &on_event)
data/lib/envoy/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Envoy
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: envoy_ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Travis Petticrew