Class: Rubino::Tools::Result
- Inherits:
-
Object
- Object
- Rubino::Tools::Result
- Defined in:
- lib/rubino/tools/result.rb
Overview
Encapsulates the result of a tool execution.
Constant Summary collapse
- ERROR_PREFIX_RE =
True when this result represents a failure for DISPLAY purposes, even when the tool didn’t raise. Many tools (read, edit, …) report a soft failure by RETURNING an “Error: …” string (status stays :success) or by setting an error_code, instead of raising. The CLI used to render those as a green “✓ done” because it only checked #success?. This is the single predicate the UI uses so an errored tool shows “✗” regardless of which failure convention the tool used.
Matches “Error:” AND the verb forms the file tools use in their rescue —“Error editing …”, “Error reading …”, “Error writing …” (note: NO colon after “Error”). The old ‘start_with?(“Error:”)` check missed those, so a failed edit (e.g. the accented-file write crash) rendered with a green ✓instead of ✗. Anchored `Error` + (`:` | whitespace) so a non-error line like “Errors found: 0” still doesn’t trip it.
/\AError[:\s]/- EMPTY_OUTPUT_PLACEHOLDER =
Substituted when a tool legitimately produces no output (e.g. ‘touch`). The string survives persistence and load_history, where nil/“” would be dropped and leave a tool_call orphaned — the provider then 400s the next turn for a tool_call with no matching tool_result.
"(no output)"- NO_CONFAB_CLAUSE =
Model-facing text per denial reason (#143). Only a real human decision may read “denied by user” — an automatic denial must name the policy that fired, otherwise a child agent reports (and propagates upward) that “the user denied my tools” when no human ever decided anything. Anti-confabulation clause (#583) appended to the human/blocked denials. A blocked tool produced NO output; without this the model can paper over the soft denial string with a fabricated “result” (e.g. answering “5” for an add it never ran). “produced NO output” + “Do NOT fabricate” attacks the confabulation; “do not retry/rephrase/substitute” is the hermes-proven evasion-blocking clause. Kept out of the doom-loop denial, which already steers the model to a different strategy.
"It was NOT run and produced NO output. Do NOT fabricate, guess, or " \ "assume its result. Do not retry the same call, rephrase it, or " \ "substitute a different tool to achieve the same effect. If this tool " \ "was required, state that the task is blocked pending approval and stop."
- DENIED_OUTPUTS =
{ user: "Tool execution denied by user. #{NO_CONFAB_CLAUSE}", policy: "Tool execution denied by policy (not by the user). #{NO_CONFAB_CLAUSE}", hardline: "Tool execution blocked by policy (hardline safety floor, not by the user): " \ "this command is never allowed. #{NO_CONFAB_CLAUSE}", permission_rule: "Tool execution blocked by policy (a configured permissions deny rule, " \ "not by the user). #{NO_CONFAB_CLAUSE}", # NOTE: keep the substring "no interactive session" — Agent::Loop's # noninteractive-block detection (loop.rb) keys the binding guard off it. noninteractive: "Tool execution BLOCKED: this tool needs approval but there is no " \ "interactive session to ask (headless/one-shot run). #{NO_CONFAB_CLAUSE} " \ "To allow it, re-run with --yolo or add it to the permissions allowlist.", doom_loop: "Tool execution blocked by the doom-loop guard (policy, not by the user): " \ "this exact call was already made repeatedly. Change strategy instead of " \ "retrying it — e.g. wait for the background-task completion notice instead " \ "of polling." }.freeze
Instance Attribute Summary collapse
-
#artifact ⇒ Object
readonly
Returns the value of attribute artifact.
-
#call_id ⇒ Object
readonly
Returns the value of attribute call_id.
-
#error ⇒ Object
readonly
Returns the value of attribute error.
-
#error_code ⇒ Object
readonly
Returns the value of attribute error_code.
-
#metrics ⇒ Object
readonly
Returns the value of attribute metrics.
-
#name ⇒ Object
readonly
Returns the value of attribute name.
-
#output ⇒ Object
readonly
Returns the value of attribute output.
-
#session_id ⇒ Object
Stamped by the ToolExecutor just before the audit write (the Result is built deep in the tool pipeline, which has no session context).
-
#status ⇒ Object
readonly
Returns the value of attribute status.
Class Method Summary collapse
- .denied(name:, call_id:, reason: :user) ⇒ Object
- .error(name:, call_id:, error:, error_code: nil) ⇒ Object
- .normalize_output(output) ⇒ Object
-
.success(name:, call_id:, output:, metrics: nil, error_code: nil, artifact: nil, transcript_card: true) ⇒ Object
Factory methods.
Instance Method Summary collapse
- #denied? ⇒ Boolean
- #errorish? ⇒ Boolean
- #failed? ⇒ Boolean
-
#initialize(name:, call_id:, output:, status:, error: nil, metrics: nil, error_code: nil, artifact: nil, transcript_card: true) ⇒ Result
constructor
‘error_code` is an optional Symbol surface for callers (UI badges, automation, future contract tests) that want to branch on the failure mode without parsing the human-facing error string.
- #success? ⇒ Boolean
- #transcript_card? ⇒ Boolean
-
#truncated_preview(max_length: 80) ⇒ Object
Returns a truncated preview for display.
Constructor Details
#initialize(name:, call_id:, output:, status:, error: nil, metrics: nil, error_code: nil, artifact: nil, transcript_card: true) ⇒ Result
‘error_code` is an optional Symbol surface for callers (UI badges, automation, future contract tests) that want to branch on the failure mode without parsing the human-facing error string. Today the canonical signal is still the output text — the symbol is a belt-and-suspenders next to it, not a replacement.
‘artifact` is an optional Hash carrying { path:, filename:, content_type:, byte_size: } when a tool produced a downloadable user-facing file. The agent loop reads this and emits an ARTIFACT_CREATED bus event so SSE consumers (the web UI, the CLI) can offer a download.
25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/rubino/tools/result.rb', line 25 def initialize(name:, call_id:, output:, status:, error: nil, metrics: nil, error_code: nil, artifact: nil, transcript_card: true) @name = name @call_id = call_id @output = output @status = status @error = error @metrics = metrics @error_code = error_code @artifact = artifact @transcript_card = transcript_card @session_id = nil end |
Instance Attribute Details
#artifact ⇒ Object (readonly)
Returns the value of attribute artifact.
7 8 9 |
# File 'lib/rubino/tools/result.rb', line 7 def artifact @artifact end |
#call_id ⇒ Object (readonly)
Returns the value of attribute call_id.
7 8 9 |
# File 'lib/rubino/tools/result.rb', line 7 def call_id @call_id end |
#error ⇒ Object (readonly)
Returns the value of attribute error.
7 8 9 |
# File 'lib/rubino/tools/result.rb', line 7 def error @error end |
#error_code ⇒ Object (readonly)
Returns the value of attribute error_code.
7 8 9 |
# File 'lib/rubino/tools/result.rb', line 7 def error_code @error_code end |
#metrics ⇒ Object (readonly)
Returns the value of attribute metrics.
7 8 9 |
# File 'lib/rubino/tools/result.rb', line 7 def metrics @metrics end |
#name ⇒ Object (readonly)
Returns the value of attribute name.
7 8 9 |
# File 'lib/rubino/tools/result.rb', line 7 def name @name end |
#output ⇒ Object (readonly)
Returns the value of attribute output.
7 8 9 |
# File 'lib/rubino/tools/result.rb', line 7 def output @output end |
#session_id ⇒ Object
Stamped by the ToolExecutor just before the audit write (the Result is built deep in the tool pipeline, which has no session context). nil for results created outside a session (one-shot / test path).
12 13 14 |
# File 'lib/rubino/tools/result.rb', line 12 def session_id @session_id end |
#status ⇒ Object (readonly)
Returns the value of attribute status.
7 8 9 |
# File 'lib/rubino/tools/result.rb', line 7 def status @status end |
Class Method Details
.denied(name:, call_id:, reason: :user) ⇒ Object
141 142 143 144 |
# File 'lib/rubino/tools/result.rb', line 141 def self.denied(name:, call_id:, reason: :user) key = DENIED_OUTPUTS.key?(reason) ? reason : :policy new(name: name, call_id: call_id, output: DENIED_OUTPUTS[key], status: :denied) end |
.error(name:, call_id:, error:, error_code: nil) ⇒ Object
99 100 101 102 103 104 |
# File 'lib/rubino/tools/result.rb', line 99 def self.error(name:, call_id:, error:, error_code: nil) msg = error.to_s msg = "unknown error" if msg.empty? new(name: name, call_id: call_id, output: "Error: #{msg}", status: :error, error: error, error_code: error_code) end |
.normalize_output(output) ⇒ Object
146 147 148 149 |
# File 'lib/rubino/tools/result.rb', line 146 def self.normalize_output(output) text = output.to_s text.empty? ? EMPTY_OUTPUT_PLACEHOLDER : text end |
.success(name:, call_id:, output:, metrics: nil, error_code: nil, artifact: nil, transcript_card: true) ⇒ Object
Factory methods
92 93 94 95 96 97 |
# File 'lib/rubino/tools/result.rb', line 92 def self.success(name:, call_id:, output:, metrics: nil, error_code: nil, artifact: nil, transcript_card: true) new(name: name, call_id: call_id, output: normalize_output(output), status: :success, metrics: metrics, error_code: error_code, artifact: artifact, transcript_card: transcript_card) end |
Instance Method Details
#denied? ⇒ Boolean
48 49 50 |
# File 'lib/rubino/tools/result.rb', line 48 def denied? @status == :denied end |
#errorish? ⇒ Boolean
72 73 74 75 76 77 |
# File 'lib/rubino/tools/result.rb', line 72 def errorish? return true unless success? return true unless @error_code.nil? ERROR_PREFIX_RE.match?(@output.to_s) end |
#failed? ⇒ Boolean
44 45 46 |
# File 'lib/rubino/tools/result.rb', line 44 def failed? @status == :error end |
#success? ⇒ Boolean
40 41 42 |
# File 'lib/rubino/tools/result.rb', line 40 def success? @status == :success end |
#transcript_card? ⇒ Boolean
52 53 54 |
# File 'lib/rubino/tools/result.rb', line 52 def transcript_card? @transcript_card != false end |
#truncated_preview(max_length: 80) ⇒ Object
Returns a truncated preview for display
80 81 82 83 |
# File 'lib/rubino/tools/result.rb', line 80 def truncated_preview(max_length: 80) text = @output.to_s text.length > max_length ? "#{text[0...max_length]}..." : text end |