sentienceapi 0.92.2__py3-none-any.whl → 0.98.0__py3-none-any.whl
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.
Potentially problematic release.
This version of sentienceapi might be problematic. Click here for more details.
- sentience/__init__.py +107 -2
- sentience/_extension_loader.py +156 -1
- sentience/action_executor.py +2 -0
- sentience/actions.py +354 -9
- sentience/agent.py +4 -0
- sentience/agent_runtime.py +840 -0
- sentience/asserts/__init__.py +70 -0
- sentience/asserts/expect.py +621 -0
- sentience/asserts/query.py +383 -0
- sentience/async_api.py +8 -1
- sentience/backends/__init__.py +137 -0
- sentience/backends/actions.py +372 -0
- sentience/backends/browser_use_adapter.py +241 -0
- sentience/backends/cdp_backend.py +393 -0
- sentience/backends/exceptions.py +211 -0
- sentience/backends/playwright_backend.py +194 -0
- sentience/backends/protocol.py +216 -0
- sentience/backends/sentience_context.py +469 -0
- sentience/backends/snapshot.py +483 -0
- sentience/browser.py +230 -74
- sentience/canonicalization.py +207 -0
- sentience/cloud_tracing.py +65 -24
- sentience/constants.py +6 -0
- sentience/cursor_policy.py +142 -0
- sentience/extension/content.js +35 -0
- sentience/extension/injected_api.js +310 -15
- sentience/extension/manifest.json +1 -1
- sentience/extension/pkg/sentience_core.d.ts +22 -22
- sentience/extension/pkg/sentience_core.js +192 -144
- sentience/extension/pkg/sentience_core_bg.wasm +0 -0
- sentience/extension/release.json +29 -29
- sentience/failure_artifacts.py +241 -0
- sentience/integrations/__init__.py +6 -0
- sentience/integrations/langchain/__init__.py +12 -0
- sentience/integrations/langchain/context.py +18 -0
- sentience/integrations/langchain/core.py +326 -0
- sentience/integrations/langchain/tools.py +180 -0
- sentience/integrations/models.py +46 -0
- sentience/integrations/pydanticai/__init__.py +15 -0
- sentience/integrations/pydanticai/deps.py +20 -0
- sentience/integrations/pydanticai/toolset.py +468 -0
- sentience/llm_provider.py +695 -18
- sentience/models.py +536 -3
- sentience/ordinal.py +280 -0
- sentience/query.py +66 -4
- sentience/schemas/trace_v1.json +27 -1
- sentience/snapshot.py +384 -93
- sentience/snapshot_diff.py +39 -54
- sentience/text_search.py +1 -0
- sentience/trace_event_builder.py +20 -1
- sentience/trace_indexing/indexer.py +3 -49
- sentience/tracer_factory.py +1 -3
- sentience/verification.py +618 -0
- sentience/visual_agent.py +3 -1
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/METADATA +198 -40
- sentienceapi-0.98.0.dist-info/RECORD +92 -0
- sentience/utils.py +0 -296
- sentienceapi-0.92.2.dist-info/RECORD +0 -65
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/WHEEL +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/entry_points.txt +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-APACHE +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/licenses/LICENSE-MIT +0 -0
- {sentienceapi-0.92.2.dist-info → sentienceapi-0.98.0.dist-info}/top_level.txt +0 -0
sentience/ordinal.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phase 3: Ordinal Intent Detection for Semantic Search
|
|
3
|
+
|
|
4
|
+
This module provides functions to detect ordinal intent in natural language goals
|
|
5
|
+
and select elements based on their position within groups.
|
|
6
|
+
|
|
7
|
+
Ordinal operators supported:
|
|
8
|
+
- Position-based: "first", "second", "third", "1st", "2nd", "3rd", etc.
|
|
9
|
+
- Relative: "top", "bottom", "last", "next", "previous"
|
|
10
|
+
- Numeric: "#1", "#2", "number 1", "item 3"
|
|
11
|
+
|
|
12
|
+
Example usage:
|
|
13
|
+
from sentience.ordinal import detect_ordinal_intent, select_by_ordinal
|
|
14
|
+
|
|
15
|
+
intent = detect_ordinal_intent("click the first search result")
|
|
16
|
+
# OrdinalIntent(kind='nth', n=1, detected=True)
|
|
17
|
+
|
|
18
|
+
element = select_by_ordinal(elements, dominant_group_key, intent)
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Literal
|
|
24
|
+
|
|
25
|
+
from sentience.models import Element
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class OrdinalIntent:
|
|
30
|
+
"""Detected ordinal intent from a goal string."""
|
|
31
|
+
|
|
32
|
+
detected: bool
|
|
33
|
+
kind: Literal["first", "last", "nth", "top_k", "next", "previous"] | None = None
|
|
34
|
+
n: int | None = None # For "nth" kind: 1-indexed position (1=first, 2=second)
|
|
35
|
+
k: int | None = None # For "top_k" kind: number of items
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Ordinal word to number mapping
|
|
39
|
+
ORDINAL_WORDS = {
|
|
40
|
+
"first": 1,
|
|
41
|
+
"second": 2,
|
|
42
|
+
"third": 3,
|
|
43
|
+
"fourth": 4,
|
|
44
|
+
"fifth": 5,
|
|
45
|
+
"sixth": 6,
|
|
46
|
+
"seventh": 7,
|
|
47
|
+
"eighth": 8,
|
|
48
|
+
"ninth": 9,
|
|
49
|
+
"tenth": 10,
|
|
50
|
+
"1st": 1,
|
|
51
|
+
"2nd": 2,
|
|
52
|
+
"3rd": 3,
|
|
53
|
+
"4th": 4,
|
|
54
|
+
"5th": 5,
|
|
55
|
+
"6th": 6,
|
|
56
|
+
"7th": 7,
|
|
57
|
+
"8th": 8,
|
|
58
|
+
"9th": 9,
|
|
59
|
+
"10th": 10,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Patterns for detecting ordinal intent
|
|
63
|
+
ORDINAL_PATTERNS = [
|
|
64
|
+
# "first", "second", etc.
|
|
65
|
+
(
|
|
66
|
+
r"\b(first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth)\b",
|
|
67
|
+
"ordinal_word",
|
|
68
|
+
),
|
|
69
|
+
# "1st", "2nd", "3rd", etc.
|
|
70
|
+
(r"\b(\d+)(st|nd|rd|th)\b", "ordinal_suffix"),
|
|
71
|
+
# "#1", "#2", etc.
|
|
72
|
+
(r"#(\d+)\b", "hash_number"),
|
|
73
|
+
# "number 1", "item 3", "result 5"
|
|
74
|
+
(r"\b(?:number|item|result|option|choice)\s*(\d+)\b", "labeled_number"),
|
|
75
|
+
# "top" (implies first/best)
|
|
76
|
+
(r"\btop\b(?!\s*\d)", "top"),
|
|
77
|
+
# "top 3", "top 5"
|
|
78
|
+
(r"\btop\s+(\d+)\b", "top_k"),
|
|
79
|
+
# "last", "final", "bottom"
|
|
80
|
+
(r"\b(last|final|bottom)\b", "last"),
|
|
81
|
+
# "next", "following"
|
|
82
|
+
(r"\b(next|following)\b", "next"),
|
|
83
|
+
# "previous", "preceding", "prior"
|
|
84
|
+
(r"\b(previous|preceding|prior)\b", "previous"),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def detect_ordinal_intent(goal: str) -> OrdinalIntent:
|
|
89
|
+
"""
|
|
90
|
+
Detect ordinal intent from a goal string.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
goal: Natural language goal (e.g., "click the first search result")
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
OrdinalIntent with detected=True if ordinal intent found, False otherwise.
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
>>> detect_ordinal_intent("click the first item")
|
|
100
|
+
OrdinalIntent(detected=True, kind='nth', n=1)
|
|
101
|
+
|
|
102
|
+
>>> detect_ordinal_intent("select the 3rd option")
|
|
103
|
+
OrdinalIntent(detected=True, kind='nth', n=3)
|
|
104
|
+
|
|
105
|
+
>>> detect_ordinal_intent("show top 5 results")
|
|
106
|
+
OrdinalIntent(detected=True, kind='top_k', k=5)
|
|
107
|
+
|
|
108
|
+
>>> detect_ordinal_intent("click the last button")
|
|
109
|
+
OrdinalIntent(detected=True, kind='last')
|
|
110
|
+
|
|
111
|
+
>>> detect_ordinal_intent("find the submit button")
|
|
112
|
+
OrdinalIntent(detected=False)
|
|
113
|
+
"""
|
|
114
|
+
goal_lower = goal.lower()
|
|
115
|
+
|
|
116
|
+
for pattern, pattern_type in ORDINAL_PATTERNS:
|
|
117
|
+
match = re.search(pattern, goal_lower, re.IGNORECASE)
|
|
118
|
+
if match:
|
|
119
|
+
if pattern_type == "ordinal_word":
|
|
120
|
+
word = match.group(1).lower()
|
|
121
|
+
n = ORDINAL_WORDS.get(word)
|
|
122
|
+
if n:
|
|
123
|
+
return OrdinalIntent(detected=True, kind="nth", n=n)
|
|
124
|
+
|
|
125
|
+
elif pattern_type == "ordinal_suffix":
|
|
126
|
+
n = int(match.group(1))
|
|
127
|
+
return OrdinalIntent(detected=True, kind="nth", n=n)
|
|
128
|
+
|
|
129
|
+
elif pattern_type == "hash_number":
|
|
130
|
+
n = int(match.group(1))
|
|
131
|
+
return OrdinalIntent(detected=True, kind="nth", n=n)
|
|
132
|
+
|
|
133
|
+
elif pattern_type == "labeled_number":
|
|
134
|
+
n = int(match.group(1))
|
|
135
|
+
return OrdinalIntent(detected=True, kind="nth", n=n)
|
|
136
|
+
|
|
137
|
+
elif pattern_type == "top":
|
|
138
|
+
# "top" without a number means "first/best"
|
|
139
|
+
return OrdinalIntent(detected=True, kind="first")
|
|
140
|
+
|
|
141
|
+
elif pattern_type == "top_k":
|
|
142
|
+
k = int(match.group(1))
|
|
143
|
+
return OrdinalIntent(detected=True, kind="top_k", k=k)
|
|
144
|
+
|
|
145
|
+
elif pattern_type == "last":
|
|
146
|
+
return OrdinalIntent(detected=True, kind="last")
|
|
147
|
+
|
|
148
|
+
elif pattern_type == "next":
|
|
149
|
+
return OrdinalIntent(detected=True, kind="next")
|
|
150
|
+
|
|
151
|
+
elif pattern_type == "previous":
|
|
152
|
+
return OrdinalIntent(detected=True, kind="previous")
|
|
153
|
+
|
|
154
|
+
return OrdinalIntent(detected=False)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def select_by_ordinal(
|
|
158
|
+
elements: list[Element],
|
|
159
|
+
dominant_group_key: str | None,
|
|
160
|
+
intent: OrdinalIntent,
|
|
161
|
+
current_element_id: int | None = None,
|
|
162
|
+
) -> Element | list[Element] | None:
|
|
163
|
+
"""
|
|
164
|
+
Select element(s) from a list based on ordinal intent.
|
|
165
|
+
|
|
166
|
+
Uses the dominant_group_key to filter to the "main content" group,
|
|
167
|
+
then selects by group_index based on the ordinal intent.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
elements: List of elements with group_key and group_index populated
|
|
171
|
+
dominant_group_key: The most common group key (main content group)
|
|
172
|
+
intent: Detected ordinal intent
|
|
173
|
+
current_element_id: Current element ID (for next/previous navigation)
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Single Element for nth/first/last, list of Elements for top_k,
|
|
177
|
+
or None if no matching element found.
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
>>> intent = OrdinalIntent(detected=True, kind='nth', n=1)
|
|
181
|
+
>>> element = select_by_ordinal(elements, "x5-w2-h1", intent)
|
|
182
|
+
# Returns element with group_key="x5-w2-h1" and group_index=0
|
|
183
|
+
"""
|
|
184
|
+
if not intent.detected:
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
# Filter to dominant group if available
|
|
188
|
+
if dominant_group_key:
|
|
189
|
+
group_elements = [e for e in elements if e.group_key == dominant_group_key]
|
|
190
|
+
else:
|
|
191
|
+
# Fallback: use all elements with group_index
|
|
192
|
+
group_elements = [e for e in elements if e.group_index is not None]
|
|
193
|
+
|
|
194
|
+
if not group_elements:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
# Sort by group_index to ensure correct ordering
|
|
198
|
+
group_elements.sort(key=lambda e: e.group_index if e.group_index is not None else 0)
|
|
199
|
+
|
|
200
|
+
if intent.kind == "first" or (intent.kind == "nth" and intent.n == 1):
|
|
201
|
+
# First element (group_index=0)
|
|
202
|
+
return group_elements[0] if group_elements else None
|
|
203
|
+
|
|
204
|
+
elif intent.kind == "nth" and intent.n is not None:
|
|
205
|
+
# Nth element (1-indexed, so n=2 means group_index=1)
|
|
206
|
+
target_index = intent.n - 1
|
|
207
|
+
if 0 <= target_index < len(group_elements):
|
|
208
|
+
return group_elements[target_index]
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
elif intent.kind == "last":
|
|
212
|
+
# Last element
|
|
213
|
+
return group_elements[-1] if group_elements else None
|
|
214
|
+
|
|
215
|
+
elif intent.kind == "top_k" and intent.k is not None:
|
|
216
|
+
# Top K elements
|
|
217
|
+
return group_elements[: intent.k]
|
|
218
|
+
|
|
219
|
+
elif intent.kind == "next" and current_element_id is not None:
|
|
220
|
+
# Next element after current
|
|
221
|
+
for i, elem in enumerate(group_elements):
|
|
222
|
+
if elem.id == current_element_id and i + 1 < len(group_elements):
|
|
223
|
+
return group_elements[i + 1]
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
elif intent.kind == "previous" and current_element_id is not None:
|
|
227
|
+
# Previous element before current
|
|
228
|
+
for i, elem in enumerate(group_elements):
|
|
229
|
+
if elem.id == current_element_id and i > 0:
|
|
230
|
+
return group_elements[i - 1]
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
return None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def boost_ordinal_elements(
|
|
237
|
+
elements: list[Element],
|
|
238
|
+
dominant_group_key: str | None,
|
|
239
|
+
intent: OrdinalIntent,
|
|
240
|
+
boost_factor: int = 10000,
|
|
241
|
+
) -> list[Element]:
|
|
242
|
+
"""
|
|
243
|
+
Boost the importance of elements matching ordinal intent.
|
|
244
|
+
|
|
245
|
+
This is useful for integrating ordinal selection with existing
|
|
246
|
+
importance-based ranking. Elements matching the ordinal intent
|
|
247
|
+
get a significant importance boost.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
elements: List of elements (not modified)
|
|
251
|
+
dominant_group_key: The most common group key
|
|
252
|
+
intent: Detected ordinal intent
|
|
253
|
+
boost_factor: Amount to add to importance (default: 10000)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
A new list with copies of elements, with boosted importance for matches.
|
|
257
|
+
"""
|
|
258
|
+
if not intent.detected or not dominant_group_key:
|
|
259
|
+
return [e.model_copy() for e in elements]
|
|
260
|
+
|
|
261
|
+
target = select_by_ordinal(elements, dominant_group_key, intent)
|
|
262
|
+
|
|
263
|
+
if target is None:
|
|
264
|
+
return [e.model_copy() for e in elements]
|
|
265
|
+
|
|
266
|
+
# Handle single element or list
|
|
267
|
+
if isinstance(target, list):
|
|
268
|
+
target_ids = {e.id for e in target}
|
|
269
|
+
else:
|
|
270
|
+
target_ids = {target.id}
|
|
271
|
+
|
|
272
|
+
# Create copies and boost matching elements
|
|
273
|
+
result = []
|
|
274
|
+
for elem in elements:
|
|
275
|
+
copy = elem.model_copy()
|
|
276
|
+
if copy.id in target_ids:
|
|
277
|
+
copy.importance = (copy.importance or 0) + boost_factor
|
|
278
|
+
result.append(copy)
|
|
279
|
+
|
|
280
|
+
return result
|
sentience/query.py
CHANGED
|
@@ -52,16 +52,28 @@ def parse_selector(selector: str) -> dict[str, Any]: # noqa: C901
|
|
|
52
52
|
query["visible"] = False
|
|
53
53
|
elif op == "~":
|
|
54
54
|
# Substring match (case-insensitive)
|
|
55
|
-
if key == "text"
|
|
55
|
+
if key == "text":
|
|
56
56
|
query["text_contains"] = value
|
|
57
|
+
elif key == "name":
|
|
58
|
+
query["name_contains"] = value
|
|
59
|
+
elif key == "value":
|
|
60
|
+
query["value_contains"] = value
|
|
57
61
|
elif op == "^=":
|
|
58
62
|
# Prefix match
|
|
59
|
-
if key == "text"
|
|
63
|
+
if key == "text":
|
|
60
64
|
query["text_prefix"] = value
|
|
65
|
+
elif key == "name":
|
|
66
|
+
query["name_prefix"] = value
|
|
67
|
+
elif key == "value":
|
|
68
|
+
query["value_prefix"] = value
|
|
61
69
|
elif op == "$=":
|
|
62
70
|
# Suffix match
|
|
63
|
-
if key == "text"
|
|
71
|
+
if key == "text":
|
|
64
72
|
query["text_suffix"] = value
|
|
73
|
+
elif key == "name":
|
|
74
|
+
query["name_suffix"] = value
|
|
75
|
+
elif key == "value":
|
|
76
|
+
query["value_suffix"] = value
|
|
65
77
|
elif op == ">":
|
|
66
78
|
# Greater than
|
|
67
79
|
if is_numeric:
|
|
@@ -116,8 +128,14 @@ def parse_selector(selector: str) -> dict[str, Any]: # noqa: C901
|
|
|
116
128
|
query["visible"] = value.lower() == "true"
|
|
117
129
|
elif key == "tag":
|
|
118
130
|
query["tag"] = value
|
|
119
|
-
elif key == "
|
|
131
|
+
elif key == "text":
|
|
120
132
|
query["text"] = value
|
|
133
|
+
elif key == "name":
|
|
134
|
+
query["name"] = value
|
|
135
|
+
elif key == "value":
|
|
136
|
+
query["value"] = value
|
|
137
|
+
elif key in ("checked", "disabled", "expanded"):
|
|
138
|
+
query[key] = value.lower() == "true"
|
|
121
139
|
elif key == "importance" and is_numeric:
|
|
122
140
|
query["importance"] = numeric_value
|
|
123
141
|
elif key.startswith("attr."):
|
|
@@ -192,6 +210,50 @@ def match_element(element: Element, query: dict[str, Any]) -> bool: # noqa: C90
|
|
|
192
210
|
if not element.text.lower().endswith(query["text_suffix"].lower()):
|
|
193
211
|
return False
|
|
194
212
|
|
|
213
|
+
# Name matching (best-effort; fallback to text for backward compatibility)
|
|
214
|
+
name_val = element.name or element.text or ""
|
|
215
|
+
if "name" in query:
|
|
216
|
+
if not name_val or name_val != query["name"]:
|
|
217
|
+
return False
|
|
218
|
+
if "name_contains" in query:
|
|
219
|
+
if not name_val or query["name_contains"].lower() not in name_val.lower():
|
|
220
|
+
return False
|
|
221
|
+
if "name_prefix" in query:
|
|
222
|
+
if not name_val or not name_val.lower().startswith(query["name_prefix"].lower()):
|
|
223
|
+
return False
|
|
224
|
+
if "name_suffix" in query:
|
|
225
|
+
if not name_val or not name_val.lower().endswith(query["name_suffix"].lower()):
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
# Value matching (inputs/textarea/select)
|
|
229
|
+
if "value" in query:
|
|
230
|
+
if element.value is None or element.value != query["value"]:
|
|
231
|
+
return False
|
|
232
|
+
if "value_contains" in query:
|
|
233
|
+
if element.value is None or query["value_contains"].lower() not in element.value.lower():
|
|
234
|
+
return False
|
|
235
|
+
if "value_prefix" in query:
|
|
236
|
+
if element.value is None or not element.value.lower().startswith(
|
|
237
|
+
query["value_prefix"].lower()
|
|
238
|
+
):
|
|
239
|
+
return False
|
|
240
|
+
if "value_suffix" in query:
|
|
241
|
+
if element.value is None or not element.value.lower().endswith(
|
|
242
|
+
query["value_suffix"].lower()
|
|
243
|
+
):
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
# State matching (best-effort)
|
|
247
|
+
if "checked" in query:
|
|
248
|
+
if (element.checked is True) != query["checked"]:
|
|
249
|
+
return False
|
|
250
|
+
if "disabled" in query:
|
|
251
|
+
if (element.disabled is True) != query["disabled"]:
|
|
252
|
+
return False
|
|
253
|
+
if "expanded" in query:
|
|
254
|
+
if (element.expanded is True) != query["expanded"]:
|
|
255
|
+
return False
|
|
256
|
+
|
|
195
257
|
# Importance filtering
|
|
196
258
|
if "importance" in query:
|
|
197
259
|
if element.importance != query["importance"]:
|
sentience/schemas/trace_v1.json
CHANGED
|
@@ -248,7 +248,24 @@
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
}
|
|
251
|
-
}
|
|
251
|
+
},
|
|
252
|
+
"assertions": {
|
|
253
|
+
"type": "array",
|
|
254
|
+
"description": "Assertion results from agent verification loop",
|
|
255
|
+
"items": {
|
|
256
|
+
"type": "object",
|
|
257
|
+
"required": ["label", "passed"],
|
|
258
|
+
"properties": {
|
|
259
|
+
"label": {"type": "string", "description": "Human-readable assertion label"},
|
|
260
|
+
"passed": {"type": "boolean", "description": "Whether the assertion passed"},
|
|
261
|
+
"required": {"type": "boolean", "description": "If true, assertion gates step success"},
|
|
262
|
+
"reason": {"type": "string", "description": "Explanation (especially when failed)"},
|
|
263
|
+
"details": {"type": "object", "description": "Additional structured data for debugging"}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
"task_done": {"type": "boolean", "description": "True if task completion assertion passed"},
|
|
268
|
+
"task_done_label": {"type": "string", "description": "Label of the task completion assertion"}
|
|
252
269
|
}
|
|
253
270
|
}
|
|
254
271
|
}
|
|
@@ -270,6 +287,15 @@
|
|
|
270
287
|
"properties": {
|
|
271
288
|
"step_id": {"type": "string"},
|
|
272
289
|
"passed": {"type": "boolean"},
|
|
290
|
+
"kind": {
|
|
291
|
+
"type": "string",
|
|
292
|
+
"enum": ["assert", "task_done"],
|
|
293
|
+
"description": "Type of verification event"
|
|
294
|
+
},
|
|
295
|
+
"label": {"type": "string", "description": "Human-readable label for the assertion"},
|
|
296
|
+
"required": {"type": "boolean", "description": "If true, assertion gates step success"},
|
|
297
|
+
"reason": {"type": "string", "description": "Explanation (especially when failed)"},
|
|
298
|
+
"details": {"type": "object", "description": "Additional structured data for debugging"},
|
|
273
299
|
"signals": {"type": "object"}
|
|
274
300
|
}
|
|
275
301
|
},
|