ask-decisions 0.2.0 → 0.2.1
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 +33 -1
- data/lib/ask/decisions/agent_adapter.rb +15 -2
- data/lib/ask/decisions/gate.rb +34 -4
- data/lib/ask/decisions/output_judge.rb +51 -8
- data/lib/ask/decisions/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: 3dd4560892d89a85e39ed1e73474268724f2f55fca39f1471e9e490e2044168c
|
|
4
|
+
data.tar.gz: d960989693373e6ce7afe0ddcec17bc88e59addafb89d5269fa67ef52bb9b299
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e2616a2e442dd2c764e5dd3d020a7ba7086f40ba24acca442e272db358817727e4bbaa6c57e84b87fea7c3e64103a6c6a741d7cac24f29f72504708a086b6ea
|
|
7
|
+
data.tar.gz: 99ea57a135e7f4265245f246f908af5b821b4780ffbf9cafe8f68c94c9d63124e4298666b106c556ccbb9c9aa1be75bb45e690747f9c063fc1616ee053553248
|
data/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,39 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
6
|
|
|
7
|
-
## [0.2.
|
|
7
|
+
## [0.2.1] - 2026-09-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **The host owns the judgement: `Gate.new(provider, questions:, thresholds:)`.**
|
|
12
|
+
The questions a gate asks are a property of the host's tools, not of the gem.
|
|
13
|
+
A booking tool and a shell tool are not dangerous for the same reason, and
|
|
14
|
+
pi-jev's questions — "is this action destructive?", "does this send local data
|
|
15
|
+
off-machine?" — say nothing useful about booking an appointment. A host now
|
|
16
|
+
asks its own: "does this commit the customer to a booking?", "does this spend
|
|
17
|
+
the owner's money?". Every question must be armed with a threshold, and a
|
|
18
|
+
question without one is refused at construction rather than armed with a bar
|
|
19
|
+
that never fires. A threshold given for one of the default questions still
|
|
20
|
+
keeps the rest of the defaults, so raising one bar stays one line.
|
|
21
|
+
|
|
22
|
+
- **`OutputJudge.new(provider, questions:, advice:)`.** Same reasoning: what an
|
|
23
|
+
output *is* — a leak, a failure class, the advice to give — belongs to the
|
|
24
|
+
host. The two ids the result reads (`:leaks_secret`, `:failure_class`) stay
|
|
25
|
+
the gem's contract, and the outcome question must offer `no_failure` among its
|
|
26
|
+
criteria, because the judge runs after every judged call and not only after a
|
|
27
|
+
suspicious one; both are refused at construction when they are missing, since
|
|
28
|
+
a judge that reads nothing judges nothing, silently.
|
|
29
|
+
|
|
30
|
+
- **`AgentAdapter` passes the judgement through** — `gate_questions`,
|
|
31
|
+
`output_questions`, `output_advice` — so a host configures the guards for its
|
|
32
|
+
own tools in one place.
|
|
33
|
+
|
|
34
|
+
### Notes
|
|
35
|
+
|
|
36
|
+
- 0.2.0 is what it was released as: the router removal below, and the
|
|
37
|
+
host-owned judgement arrived after it.
|
|
38
|
+
|
|
39
|
+
## [0.2.0] - 2026-09-17
|
|
8
40
|
|
|
9
41
|
### Removed
|
|
10
42
|
|
|
@@ -106,12 +106,25 @@ module Ask
|
|
|
106
106
|
Ask::Decisions.resolve_provider(name)
|
|
107
107
|
end
|
|
108
108
|
|
|
109
|
+
# What the host judges, and how high the bar is. The questions are the
|
|
110
|
+
# host's because risk is: a booking tool and a shell tool are not
|
|
111
|
+
# dangerous for the same reason, and a gate written for one says nothing
|
|
112
|
+
# useful about the other.
|
|
109
113
|
def gate_config
|
|
110
|
-
{
|
|
114
|
+
{
|
|
115
|
+
questions: @config[:gate_questions],
|
|
116
|
+
thresholds: @config[:gate_thresholds],
|
|
117
|
+
tools: @config[:gate_tools]
|
|
118
|
+
}.compact
|
|
111
119
|
end
|
|
112
120
|
|
|
113
121
|
def output_judge_config
|
|
114
|
-
{
|
|
122
|
+
{
|
|
123
|
+
questions: @config[:output_questions],
|
|
124
|
+
advice: @config[:output_advice],
|
|
125
|
+
tools: @config[:output_tools],
|
|
126
|
+
output_limit: @config[:output_limit]
|
|
127
|
+
}.compact
|
|
115
128
|
end
|
|
116
129
|
|
|
117
130
|
def failure_classifier_config
|
data/lib/ask/decisions/gate.rb
CHANGED
|
@@ -49,11 +49,20 @@ module Ask
|
|
|
49
49
|
}.freeze
|
|
50
50
|
|
|
51
51
|
# @param provider [Ask::DecisionProvider] the decision provider to use
|
|
52
|
-
# @param
|
|
52
|
+
# @param questions [Hash{Symbol => Decision::Noul,Decision::Score}] what
|
|
53
|
+
# to ask about a call. The defaults above are pi-jev's, written for a
|
|
54
|
+
# coding agent's tools; a host with other tools should say what risk
|
|
55
|
+
# means for them ("does this commit the customer to a booking?", "does
|
|
56
|
+
# this spend the owner's money?") rather than inherit a vocabulary
|
|
57
|
+
# about shell commands.
|
|
58
|
+
# @param thresholds [Hash{Symbol => Numeric}] the bar for each question.
|
|
59
|
+
# Every question needs one: a question with no threshold can never
|
|
60
|
+
# flag, and a gate that looks armed and never fires is worse than none.
|
|
53
61
|
# @param tools [Array<String>, nil] tools to gate (nil = all)
|
|
54
|
-
def initialize(provider, thresholds:
|
|
62
|
+
def initialize(provider, questions: QUESTIONS, thresholds: DEFAULT_THRESHOLDS, tools: nil)
|
|
55
63
|
@provider = provider
|
|
56
|
-
@
|
|
64
|
+
@questions = questions
|
|
65
|
+
@thresholds = arming_thresholds(questions, thresholds)
|
|
57
66
|
@tools = tools
|
|
58
67
|
end
|
|
59
68
|
|
|
@@ -71,7 +80,7 @@ module Ask
|
|
|
71
80
|
|
|
72
81
|
result = @provider.evaluate(
|
|
73
82
|
state: state,
|
|
74
|
-
decisions:
|
|
83
|
+
decisions: @questions
|
|
75
84
|
)
|
|
76
85
|
|
|
77
86
|
Verdict.new(result, @thresholds)
|
|
@@ -79,6 +88,27 @@ module Ask
|
|
|
79
88
|
|
|
80
89
|
private
|
|
81
90
|
|
|
91
|
+
# Every question must be armed. A host that supplies its own questions
|
|
92
|
+
# and forgets a threshold would otherwise get a gate that silently
|
|
93
|
+
# ignores one of its own risk questions, which is the failure mode a
|
|
94
|
+
# gate exists to prevent.
|
|
95
|
+
#
|
|
96
|
+
# A threshold given for a question the defaults also ask keeps the rest
|
|
97
|
+
# of the defaults, so raising one bar is one line rather than a copy of
|
|
98
|
+
# the table.
|
|
99
|
+
def arming_thresholds(questions, thresholds)
|
|
100
|
+
keys = questions.keys.map(&:to_sym)
|
|
101
|
+
armed = DEFAULT_THRESHOLDS.slice(*keys).merge(thresholds.to_h.transform_keys(&:to_sym))
|
|
102
|
+
unarmed = keys - armed.keys
|
|
103
|
+
|
|
104
|
+
unless unarmed.empty?
|
|
105
|
+
raise ArgumentError,
|
|
106
|
+
"no threshold for #{unarmed.inspect}: a question that cannot fire is not a gate"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
armed
|
|
110
|
+
end
|
|
111
|
+
|
|
82
112
|
def build_state(tool:, args:, working_dir: nil, user_message: nil)
|
|
83
113
|
{
|
|
84
114
|
tool: tool,
|
|
@@ -46,12 +46,35 @@ module Ask
|
|
|
46
46
|
}.freeze
|
|
47
47
|
|
|
48
48
|
# @param provider [Ask::DecisionProvider]
|
|
49
|
-
#
|
|
49
|
+
# The class that means nothing went wrong. A host's outcome question has
|
|
50
|
+
# to offer it, because the judge runs after every judged call and not
|
|
51
|
+
# only after a suspicious one: without it, every successful call would
|
|
52
|
+
# read as a failure.
|
|
53
|
+
SUCCESS_CLASS = "no_failure"
|
|
54
|
+
|
|
55
|
+
# A host judges two things about an output: whether it leaked something
|
|
56
|
+
# (`:leaks_secret`), and what happened (`:failure_class`). Those two ids
|
|
57
|
+
# are the gem's contract and stay fixed; the questions' words, the
|
|
58
|
+
# classes they can answer with, and the advice per class are the host's —
|
|
59
|
+
# a coding agent's failures are code bugs and broken environments, a
|
|
60
|
+
# business's are "we don't offer that" and "the system is down".
|
|
61
|
+
#
|
|
62
|
+
# @param questions [Hash{Symbol => Decision}] what to ask about a result.
|
|
63
|
+
# Must answer under :leaks_secret and :failure_class, and the outcome
|
|
64
|
+
# question must offer the class above among its criteria.
|
|
65
|
+
# @param advice [Hash{String => String,nil}] one line per class the
|
|
66
|
+
# outcome question can answer with. A class with no line is advice the
|
|
67
|
+
# model does not get.
|
|
68
|
+
# @param tools [Array<String>, nil] tools to judge (nil = the gem's own
|
|
69
|
+
# default, the coding agent's shell tool — pass the host's own)
|
|
50
70
|
# @param leak_threshold [Float] noul threshold for leak detection
|
|
51
71
|
# @param failure_threshold [Float] confidence threshold for failure classification
|
|
52
72
|
# @param output_limit [Integer] max characters of output to send
|
|
53
|
-
def initialize(provider,
|
|
73
|
+
def initialize(provider, questions: QUESTIONS, advice: ADVICE, tools: nil,
|
|
74
|
+
leak_threshold: 0.90, failure_threshold: 0.60, output_limit: 2000)
|
|
54
75
|
@provider = provider
|
|
76
|
+
@questions = judgeable(questions)
|
|
77
|
+
@advice = advice
|
|
55
78
|
@tools = tools || ["bash"]
|
|
56
79
|
@leak_threshold = leak_threshold
|
|
57
80
|
@failure_threshold = failure_threshold
|
|
@@ -70,12 +93,32 @@ module Ask
|
|
|
70
93
|
truncated = truncate(output, @output_limit)
|
|
71
94
|
state = { output: truncated, tool_arguments: truncate_values(args, 400) }
|
|
72
95
|
|
|
73
|
-
result = @provider.evaluate(state: state, decisions:
|
|
74
|
-
OutputResult.new(result, @leak_threshold, @failure_threshold)
|
|
96
|
+
result = @provider.evaluate(state: state, decisions: @questions)
|
|
97
|
+
OutputResult.new(result, @leak_threshold, @failure_threshold, advice: @advice)
|
|
75
98
|
end
|
|
76
99
|
|
|
77
100
|
private
|
|
78
101
|
|
|
102
|
+
# A host's questions have to answer the two things the result reads, and
|
|
103
|
+
# the outcome question has to be able to say that nothing went wrong.
|
|
104
|
+
# Both are silent failures otherwise — a judge that reads nothing judges
|
|
105
|
+
# nothing — so they are refused at construction instead.
|
|
106
|
+
def judgeable(questions)
|
|
107
|
+
missing = %i[leaks_secret failure_class] - questions.keys.map(&:to_sym)
|
|
108
|
+
unless missing.empty?
|
|
109
|
+
raise ArgumentError, "the output judge needs questions for #{missing.inspect}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
criteria = Array(questions[:failure_class].criteria&.keys)
|
|
113
|
+
unless criteria.include?(SUCCESS_CLASS)
|
|
114
|
+
raise ArgumentError,
|
|
115
|
+
"the failure_class question must offer #{SUCCESS_CLASS.inspect} among its criteria, " \
|
|
116
|
+
"or every successful call reads as a failure"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
questions
|
|
120
|
+
end
|
|
121
|
+
|
|
79
122
|
def truncate(str, limit)
|
|
80
123
|
return "" if str.nil?
|
|
81
124
|
str.length > limit ? "#{str[0, limit]}…[#{str.length - limit} chars elided]" : str
|
|
@@ -91,17 +134,17 @@ module Ask
|
|
|
91
134
|
class OutputResult
|
|
92
135
|
attr_reader :leak_noul, :failure_class, :failure_confidence, :advice
|
|
93
136
|
|
|
94
|
-
def initialize(batch, leak_threshold, failure_threshold)
|
|
137
|
+
def initialize(batch, leak_threshold, failure_threshold, advice: ADVICE)
|
|
95
138
|
leak_answer = batch["leaks_secret"]
|
|
96
139
|
failure_answer = batch["failure_class"]
|
|
97
140
|
|
|
98
141
|
@leak_noul = leak_answer&.noul || 0.0
|
|
99
142
|
@leak_threshold = leak_threshold
|
|
100
143
|
|
|
101
|
-
@failure_class = failure_answer&.choice ||
|
|
144
|
+
@failure_class = failure_answer&.choice || SUCCESS_CLASS
|
|
102
145
|
@failure_confidence = failure_answer&.confidence || 0.0
|
|
103
146
|
@failure_threshold = failure_threshold
|
|
104
|
-
@advice =
|
|
147
|
+
@advice = advice[@failure_class]
|
|
105
148
|
end
|
|
106
149
|
|
|
107
150
|
def leak?
|
|
@@ -109,7 +152,7 @@ module Ask
|
|
|
109
152
|
end
|
|
110
153
|
|
|
111
154
|
def failure?
|
|
112
|
-
@failure_class !=
|
|
155
|
+
@failure_class != SUCCESS_CLASS && @failure_confidence >= @failure_threshold
|
|
113
156
|
end
|
|
114
157
|
|
|
115
158
|
def to_s
|