jev-decide 0.1.0__tar.gz

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.
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: jev-usage
3
+ description: Decide when and how to call `jev decide` while driving a browser with `ziniao-cli`. Covers the call contract (page snapshot in, one typed operation + target out), the caller's execution mapping, per-call cost budget, retry policy, post-execution verification, and the anti-patterns measured on real store tasks. Use when driving a ZClaw/ziniao store browser step by step, when picking an element to click/type/scroll on an unfamiliar page, when the target can only be described semantically, or when judging whether a hard-coded selector is cheaper than a decision call.
4
+ ---
5
+
6
+ # jev-usage
7
+
8
+ `jev decide` 是**单步决策器**:喂一份 `ziniao-cli page snapshot`,拿回一个类型化操作 + 一个目标,执行由调用方负责。它不提效 —— 它只在**目标不明确**时把"猜选择器"这一步变成有置信度的判断。按下面的判据用它,否则直接写选择器更便宜。
9
+
10
+ ## 1. 该不该叫(速查)
11
+
12
+ | 场景 | 叫 jev? | 理由 |
13
+ |---|---|---|
14
+ | 首次进入陌生页面,找入口/目标元素 | **叫** | 元素表未知,猜选择器要试错 3–5 步 |
15
+ | 目标只能用语义描述,且没有稳定唯一 ID | **叫** | 这是它唯一不可替代的能力 |
16
+ | 候选多且相似(导航、图标按钮、多个下拉) | **叫** | 元素表里 40+ 候选,人挑易错 |
17
+ | 需要在 `CLICK` / `WAIT` / `SCROLL` 之间判断(懒加载、列表刷新中) | **叫** | 操作类型本身是决策 |
18
+ | 异常分支:登录墙、验证码、遮挡弹窗 | **叫**(期望 `BLOCKED`) | 早点停手比乱点便宜 |
19
+ | 判断"是否已完成" | **叫**(`DONE`),但见 §5 | `DONE` 是模型判断,不是证据 |
20
+ | 已知唯一 ID 的元素(`#rating-dropdown`) | **不叫** | 决策没有信息增量,纯开销 |
21
+ | 固定流程的翻页 / 滚动 / 轮询循环 | **不叫** | 固化进脚本,别每轮问一遍 |
22
+ | 数据抽取、正文解析、结构化输出 | **不叫** | jev 不产出内容,用 `page query` / `page exec` |
23
+ | 需要多步规划 | **不叫** | 它只做单步,多次调用不会合成计划 |
24
+ | 同一个 goal 刚问过且页面没变 | **不叫** | 结果一样 |
25
+
26
+ ## 2. 调用契约
27
+
28
+ ```bash
29
+ ziniao-cli page snapshot --store-id "$STORE" --max-items 120 \
30
+ | jev decide --goal "把评价时间范围切换为最近 1 个月"
31
+ # 或:jev decide --observation /tmp/obs.json --goal "..." --compact
32
+ ```
33
+
34
+ - **观测源只能是 `page snapshot`**。`page content --content-format structured` 读起来更好,但它的 `selectorHint` 常常只是标签名、表单控件没有定位符,**不能驱动选择**。抽取用它,决策用 snapshot。
35
+ - **退出码就是接口**:`0` 有决策 / `2` 观测不可用 / `3` 模型或凭据失败;stdout 永远是一份 JSON,直接解析。
36
+ - **ref 会过期**:`ref` 绑定具体 store + tab + 页面,导航、重载、URL 变化后失效 → **每次决策前重新快照**,不要跨导航复用 ref。
37
+ - **`--max-items` 要给足**:实测 `3` 和 `5` 都只回 10 个元素,`20` 才回 20;但它同时是成本旋钮(§4)。
38
+ - `--history FILE` 可传前几步,用于同一目标下的连续决策。
39
+
40
+ ## 3. 执行映射(调用方负责,jev 不碰)
41
+
42
+ | 决策 | 怎么执行 |
43
+ |---|---|
44
+ | `CLICK` | `ziniao-cli page click --store-id "$STORE" --ref <ref>`(或 `--selector`)。**原生 `<select>` 例外**:`page click` 驱动不了系统下拉,改为 `page exec` 置 `value` + 派发 `change`/`input` |
45
+ | `TYPE_TEXT` | 文本由调用方从自己上下文提供:`page input --selector <selector> --text <value> --clear` |
46
+ | `SELECT` | 按返回的 `selector` 置值(`page input` 没有 `--ref`) |
47
+ | `SCROLL_DOWN` / `SCROLL_UP` | `page scroll --store-id "$STORE" --y ±560` |
48
+ | `WAIT` | 不动作,重新观测 |
49
+ | `DONE` / `BLOCKED` | 停手。`DONE` 必须自己验证(§5) |
50
+
51
+ 返回体里的 `elements` 是完整索引表(含每个目标的 `ref` + `selector`),可以直接拿它执行或复核,不必为了执行再快照一次。
52
+
53
+ ## 4. 成本与预算
54
+
55
+ - 单次 `decide` ≈ **0.3–7k 未缓存输入**(随元素表大小线性涨)+ 0.3–0.5k 输出,耗时 0.8–5s。
56
+ - 经验换算:**一次 `decide` 的账单量级 ≈ 一个 agent 推理步**。所以它**只有替代掉 ≥1 个 agent 步才不亏**;替代不了就是纯开销。
57
+ - 每个决策都有固定开销:`快照(+1 调用) → decide → 执行 → 验证`。比"直接用选择器执行"**多 1 步**。已知 ID 的场合这就是净亏。
58
+ - 省钱手段:`--max-items` 收到 60–120;别把 snapshot 当轮询;同一页面同一 goal 不要重复问。
59
+ - 把决策落进产物做审计轨迹:`goal / operation / choice / selector / confidence / usage`。
60
+
61
+ **实测基准**(亚马逊店铺任务,同工作区对照):
62
+
63
+ | 任务 | jev 调用 | 占任务 token | 占任务时间 | 省下的步数 |
64
+ |---|---|---|---|---|
65
+ | 30 天评论抓取 | 5 × `decide` + `doctor` | 1.26% | 2.1% | 0(每次 +1 步) |
66
+ | 近半年差评报告 | 2 × `decide` + `doctor` | 0.61% | 3.3% | 0 |
67
+
68
+ 两次任务里 7 个成功决策有 4 个问的是同一个 `#rating-dropdown` —— 这就是"重复问"的浪费。
69
+
70
+ ## 5. 验证(不可省)
71
+
72
+ - **`DONE` 不是证据。** 实测:模型给 `DONE` 后,真实完整性是靠外部事实确认的 —— 页面计数 `5 total ratings, 5 with feedback` 等于列表 5 条,且点 Next 无新条目。
73
+ - 每次执行后**重新观测或读状态**确认页面真的变了,而不是假设点击生效。
74
+ - 抓数任务要与页面自带统计**对账**(页面面板 vs 抓取条数,差额要能解释,例如被屏蔽条目)。
75
+ - 尽量用**两条独立路径**交叉验证(例:DOM 翻页 vs 页面自身接口),逐条比对。
76
+ - 决策落地失败也要记账:实测 1/6 次 `CLICK` 因 ref 随重渲染失效报 `ELEMENT_NOT_INTERACTABLE`,最后改用 JS 兜底。
77
+
78
+ ## 6. 失败与重试
79
+
80
+ - 实测 **2/9 次 `decide` 报模型连接失败**:`{"status":"error","error":{"type":"model","message":"Model connection failed..."}}`,exit 3;同刻 `doctor` 的模型延迟从 1.0s 涨到 5.2s。
81
+ - 处理:退避 3s,**只读的 `decide` 可重试 ≤2 次**;仍失败就停下报告,**不要改成"猜一个选择器硬点"**。
82
+ - 故障区分:`jev doctor`(runtime / install / credentials / model 四项,含一次最小模型请求)能分清"服务不可达"与"凭据/模型错";凭据问题给 exit 3 + `{"error":{"type":"credential"}}`。`doctor` 不回显密钥,只看来源和长度。
83
+ - **mutation 不重试**:点击/输入/提交这类写操作的重试属于调用方的业务决策,不要因为"再问一次 jev"就重放。
84
+
85
+ ## 7. 反模式(全部来自实测)
86
+
87
+ 1. 每次切筛选都问一遍 jev(同一个下拉问了 4 次,零信息增量)。
88
+ 2. 对已知唯一 ID 的元素问 jev。
89
+ 3. 把 `page snapshot` 当页面轮询。
90
+ 4. 跨导航 / DOM 重渲染复用 `ref`。
91
+ 5. 假设 `CLICK` 一定能直接执行(原生 `<select>`、`hide-content` 的隐藏 anchor 都不行)。
92
+ 6. 用很小的 `--max-items` 来省 token(会隐藏目标,且不线性减少返回量)。
93
+ 7. 把 `DONE` 当成"任务完成"的证据。
94
+
95
+ ## 8. 最小循环骨架
96
+
97
+ ```bash
98
+ STORE=...; GOAL=...
99
+
100
+ ziniao-cli page snapshot --store-id "$STORE" --max-items 120 > /tmp/obs.json # 1 观测
101
+ jev decide --observation /tmp/obs.json --goal "$GOAL" --compact > /tmp/d.json # 2 决策(失败退避重试 ≤2)
102
+ # 3 按 §3 映射执行;写操作不重试
103
+ # 4 验证:重新 snapshot / 读页面状态,别信 DONE
104
+ # 5 审计:把 /tmp/d.json 的 operation/choice/selector/confidence/usage 追加进产物
105
+ ```
106
+
107
+ 先按 §1 判断"这一跳到底要不要问":**能写死就写死,写不死才问 jev。**
108
+
109
+ ## 9. 边界
110
+
111
+ - jev 只输出"单步操作 + 目标",**不生成文本、不产出选择器、不做多步规划、不抽取数据**。
112
+ - 本 skill 只规定调用侧行为,不改动 `jev-decide` 包本身:该包内不引入浏览器驱动、bridge 客户端或文本模型。
@@ -0,0 +1,8 @@
1
+ # Copy to ./.env, or to ~/.config/jev/.env so a CLI started by a daemon, a GUI, or
2
+ # another agent finds it without a shell rc file (agents do not read ~/.bashrc).
3
+ # The process environment always wins over both files.
4
+ TYPESAFE_API_KEY=
5
+ TYPESAFE_MODEL=jev-latest
6
+ # Decision service endpoint. Leave empty to use the default:
7
+ # https://api.typesafe.ai/v1/systemone
8
+ TYPESAFE_BASE_URL=
@@ -0,0 +1,7 @@
1
+ .env
2
+ .venv/
3
+ dist/
4
+ __pycache__/
5
+ *.py[cod]
6
+ .pytest_cache/
7
+ .ruff_cache/
@@ -0,0 +1,19 @@
1
+ # jev-decide
2
+
3
+ Read README.md before editing. This package is one thing: page state in, one typed
4
+ operation and target out.
5
+
6
+ - `jev` has exactly two commands: `decide`, the only one that decides, and `doctor`, which only diagnoses it. Keep `jev` with no arguments printing help and exiting 2.
7
+ - `decide` observes nothing, executes nothing, and never generates text. Never add a browser driver, a bridge client, or a text model here — the caller owns all three.
8
+ - `doctor` observes nothing and executes nothing either. It may make one minimal model request to prove the service answers, and it must never echo a credential — only its source and length.
9
+ - The observation layer accepts raw `ziniao-cli page snapshot` output — `page` is a required subcommand. Keep normalization in observation.py, and keep executable locators (`ref` and `selector`) on every target. `page content --content-format structured` carries no `ref` and its `selectorHint` is usually a bare tag name, so it is not an observation source.
10
+ - Targets must map to observed elements and supported operations. Never let the model emit selectors or executable code.
11
+ - Exit codes are the interface: `0` a decision or a healthy `doctor`, `2` a bad observation or bad usage, `3` the model or its credential failed. Keep stdout parseable JSON on every path, including failures.
12
+ - Credentials come from env.py: the process environment wins, then `./.env`, then `~/.config/jev/.env`. Never read a shell rc file.
13
+ - Never retry a mutation — the caller performs those. A read that fails while a document settles may be retried.
14
+ - Tests must not call paid APIs, open a browser, or open a ZClaw connection. Use the captured fixtures in tests/fixtures.
15
+ - Reinstalling the CLI must never silently install older code. `uv tool install --force` can reuse a cached wheel: verify the installed copy, or install `--editable`/`--no-cache`.
16
+ - This package owns the `jev` console script. Do not also declare it elsewhere.
17
+ - Do not commit or push unless the user requests it.
18
+
19
+ Checks: uv run ruff check ., uv run pytest, uv build.
@@ -0,0 +1,197 @@
1
+ Metadata-Version: 2.5
2
+ Name: jev-decide
3
+ Version: 0.1.0
4
+ Summary: The jev decision service: page state in, one typed operation and target out.
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: httpx[http2]<1,>=0.28
8
+ Description-Content-Type: text/markdown
9
+
10
+ # jev-decide
11
+
12
+ **One decision, no browser.** `jev decide` reads a page state and answers with a single
13
+ typed operation and target that the caller executes. It observes nothing, executes
14
+ nothing, and never generates text.
15
+
16
+ It is built for an agent that already drives a browser: page state in, one action out.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ uv tool install --editable . # tracks this checkout; right while developing
22
+ uv tool install --no-cache . # frozen copy of the current source
23
+ ```
24
+
25
+ Pass `--no-cache` for a frozen copy: `uv tool install --force .` can reuse a cached wheel
26
+ and silently install older code than the checkout.
27
+
28
+ The only runtime dependency is `httpx`. There is no browser driver here.
29
+
30
+ ## Use
31
+
32
+ Feed it the output of `ziniao-cli page snapshot` verbatim:
33
+
34
+ ```bash
35
+ ziniao-cli page snapshot --store-id "$STORE" --max-items 200 \
36
+ | jev decide --goal "Open the orders page"
37
+ ```
38
+
39
+ `page` is not optional and not implicit. The command is `ziniao-cli page snapshot`;
40
+ `ziniao-cli snapshot` answers `unknown command "snapshot" for "ziniao-cli"`, and the store
41
+ browser has to be open already.
42
+
43
+ ```json
44
+ {
45
+ "status": "ok",
46
+ "operation": "CLICK",
47
+ "choice": "e30",
48
+ "target": {
49
+ "index": "13",
50
+ "ref": "e30",
51
+ "selector": "#nav-orders",
52
+ "role": "link",
53
+ "label": "Returns & Orders"
54
+ },
55
+ "needs_text": false,
56
+ "confidence": 0.99,
57
+ "operation_probabilities": {"CLICK": 1.0, "TYPE_TEXT": 0.0, "WAIT": 0.0, "DONE": 0.0, "SCROLL_DOWN": 0.0, "BLOCKED": 0.0},
58
+ "target_probabilities": {"13": 0.95, "11": 0.03, "12": 0.02},
59
+ "elements": [{"index": "13", "ref": "e30", "selector": "#nav-orders", "role": "link", "label": "Returns & Orders", "operations": ["CLICK"]}]
60
+ }
61
+ ```
62
+
63
+ That response is real, not illustrative: the captured `ziniao-cli page snapshot` of a
64
+ public amazon.com page (`tests/fixtures/ziniao_page_snapshot.json`) with the goal *"Open
65
+ the orders page"* — `jev-1.13.0`, 2724 input tokens, 935 ms. The fixture ships in this
66
+ repo, so the shape above is reproducible offline.
67
+
68
+ - `--observation FILE` reads a file instead of stdin; `--history FILE` passes previous
69
+ steps as a JSON list; `--compact` emits one line.
70
+ - stdout is **always** JSON, including failures, so a caller parses one stream.
71
+ - Exit codes: `0` a decision, `2` the observation was unusable, `3` the model or its
72
+ credential failed.
73
+
74
+ Every response carries the whole `elements` index table with a `ref` and a `selector` for
75
+ each target, so the caller can execute or re-check without re-snapshotting.
76
+
77
+ ### Execute the decision
78
+
79
+ | Decision | How the caller runs it |
80
+ | --- | --- |
81
+ | `CLICK` | `ziniao-cli page click --store-id "$STORE" --ref <ref>` (or `--selector`) |
82
+ | `TYPE_TEXT` | the caller writes the value, then `ziniao-cli page input --store-id "$STORE" --selector <selector> --text <value> --clear` |
83
+ | `SELECT` | set the value at the returned `selector` (`page input` has no `--ref`) |
84
+ | `SCROLL_DOWN` / `SCROLL_UP` | `ziniao-cli page scroll --store-id "$STORE" --y ±560` |
85
+ | `WAIT` | observe again without acting |
86
+ | `DONE` / `BLOCKED` | stop. `DONE` is the model's choice, not proof: verify the outcome yourself |
87
+
88
+ **TYPE_TEXT never comes with text.** The service names the field and stops; the calling
89
+ agent supplies the value from its own context. That is why this package needs no text
90
+ model and no `TEXT_MODEL_API_KEY`.
91
+
92
+ ## The observation
93
+
94
+ `page snapshot` is the only `ziniao-cli` page command that returns an indexed element
95
+ table with both semantic identity (role, accessible name, context, geometry) **and**
96
+ actionable locators. `page content --content-format structured` reads better but its
97
+ `selectorHint` is often just a tag name and form controls carry no locator, so it cannot
98
+ drive a choice.
99
+
100
+ The current `ziniao-page` skill draws the same line from the other side: it treats
101
+ `page snapshot` as a local fallback — for upload controls, custom or icon-only controls,
102
+ and candidates that survive `page query` and open-shadow-DOM checks — and prefers
103
+ `page content --content-format structured` for *understanding* a page. That preference is
104
+ about reading, not about choosing: only the snapshot carries a `ref` and a real
105
+ `selector`, so a decision still needs one. Take a snapshot when a decision is due; do not
106
+ turn it into a page poll.
107
+
108
+ - Supply `--max-items` generously. The cap is the observation, so a low cap hides targets —
109
+ and a small value is not a reliable way to keep the observation short: measured against a
110
+ live store, `--max-items 3` and `--max-items 5` both returned 10 elements while `20`
111
+ returned 20.
112
+ - A `ref` belongs to one store, tab, and page, and expires on reload or any URL change.
113
+ Re-snapshot after a navigation instead of reusing `ref`; `--target-id` picks the tab in a
114
+ multi-tab store.
115
+ - Off-canvas elements are dropped the same way the browser-side reader drops them. An
116
+ element hidden with `left:-9999px` has real width and height, so geometry is the only
117
+ signal; without it, "skip to content" links become targets.
118
+ - A `<select>` is offered as a dropdown to open, because the snapshot names it without
119
+ listing its options. Add an `options` array to that element in a normalized observation
120
+ to get the `SELECT` operation.
121
+ - A page snapshot carries no scroll position, so the service offers scrolling down only.
122
+ Add `scroll: {"y":…, "height":…, "viewport":…}` to make both directions exact.
123
+ - Page text is synthesized from the element table when the caller supplies none; merge a
124
+ `text` field into the payload, or send a normalized observation, for real page prose.
125
+
126
+ ## Credentials
127
+
128
+ `TYPESAFE_API_KEY` is required. The process environment wins, then files fill in what is
129
+ missing, most specific first:
130
+
131
+ | Order | Location |
132
+ | --- | --- |
133
+ | 1 | the process environment |
134
+ | 2 | `./.env` |
135
+ | 3 | `~/.config/jev/.env` (or `$XDG_CONFIG_HOME/jev/.env`) |
136
+
137
+ The third location exists so a CLI started by a daemon, a GUI, or another agent finds its
138
+ credentials without a shell rc file — **an agent process does not read `~/.bashrc`**.
139
+ A missing key needs no separate check command: `decide` answers with exit `3` and a
140
+ parseable `{"error":{"type":"credential"}}`.
141
+
142
+ The decision endpoint is configuration, not a credential, but it resolves through the same
143
+ three locations: `TYPESAFE_BASE_URL` overrides the default
144
+ `https://api.typesafe.ai/v1/systemone`, and an empty value means the default. `doctor`
145
+ reports the endpoint it would use in both the `credentials` and `model` checks.
146
+
147
+ ```bash
148
+ mkdir -p ~/.config/jev && cp .env.example ~/.config/jev/.env
149
+ ```
150
+
151
+ ## Doctor
152
+
153
+ ```bash
154
+ jev doctor
155
+ ```
156
+
157
+ One read-only self-check for the whole install, and the fastest way to answer "why did
158
+ `decide` exit 3?":
159
+
160
+ | Check | What it reports |
161
+ | --- | --- |
162
+ | `runtime` | the interpreter actually running, and the version this package requires |
163
+ | `install` | the version, and whether this is the editable checkout or a frozen install |
164
+ | `credentials` | **where** the key came from (process environment, `./.env`, `~/.config/jev/.env`) and **how long** it is — never the key itself |
165
+ | `model` | one minimal model request that proves the service answers |
166
+
167
+ The last check is a real request on purpose: an unreachable service and a wrong decision
168
+ look identical from the caller's side, so `doctor` asks a question, names the answer it
169
+ expects, and fails if something else comes back — a reachable endpoint answering with the
170
+ wrong model is a failure, not a pass. The request carries no page state and no goal and
171
+ costs roughly 400 input tokens.
172
+
173
+ The same run, one line per check, detail trimmed:
174
+
175
+ ```json
176
+ {"status": "ok", "command": "doctor", "checks": [
177
+ {"name": "runtime", "status": "ok", "detail": {"python": "3.13.12", "required": "3.12"}},
178
+ {"name": "install", "status": "ok", "detail": {"version": "0.1.0", "mode": "editable"}},
179
+ {"name": "credentials", "status": "ok", "detail": {"source": "~/.config/jev/.env", "key": {"present": true, "length": 108}, "endpoint": "https://api.typesafe.ai/v1/systemone"}},
180
+ {"name": "model", "status": "ok", "detail": {"endpoint": "https://api.typesafe.ai/v1/systemone", "answer": "PONG", "model": "jev-1.13.0", "latency_ms": 1025}}
181
+ ], "summary": {"passed": 4, "failed": 0, "skipped": 0}}
182
+ ```
183
+
184
+ Exit `0` when every check passes, `3` when this install cannot decide. Same rule as
185
+ `decide`: stdout is always one parseable JSON document, and `--compact` emits it on one
186
+ line. `doctor` never reads an observation, so it needs no stdin.
187
+
188
+ ## Development
189
+
190
+ ```bash
191
+ uv run ruff check .
192
+ uv run pytest
193
+ uv build
194
+ ```
195
+
196
+ Tests are offline: they use a captured page snapshot and a fake model call. Nothing in
197
+ `pytest` reaches the network, opens a browser, or spends money.
@@ -0,0 +1,188 @@
1
+ # jev-decide
2
+
3
+ **One decision, no browser.** `jev decide` reads a page state and answers with a single
4
+ typed operation and target that the caller executes. It observes nothing, executes
5
+ nothing, and never generates text.
6
+
7
+ It is built for an agent that already drives a browser: page state in, one action out.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ uv tool install --editable . # tracks this checkout; right while developing
13
+ uv tool install --no-cache . # frozen copy of the current source
14
+ ```
15
+
16
+ Pass `--no-cache` for a frozen copy: `uv tool install --force .` can reuse a cached wheel
17
+ and silently install older code than the checkout.
18
+
19
+ The only runtime dependency is `httpx`. There is no browser driver here.
20
+
21
+ ## Use
22
+
23
+ Feed it the output of `ziniao-cli page snapshot` verbatim:
24
+
25
+ ```bash
26
+ ziniao-cli page snapshot --store-id "$STORE" --max-items 200 \
27
+ | jev decide --goal "Open the orders page"
28
+ ```
29
+
30
+ `page` is not optional and not implicit. The command is `ziniao-cli page snapshot`;
31
+ `ziniao-cli snapshot` answers `unknown command "snapshot" for "ziniao-cli"`, and the store
32
+ browser has to be open already.
33
+
34
+ ```json
35
+ {
36
+ "status": "ok",
37
+ "operation": "CLICK",
38
+ "choice": "e30",
39
+ "target": {
40
+ "index": "13",
41
+ "ref": "e30",
42
+ "selector": "#nav-orders",
43
+ "role": "link",
44
+ "label": "Returns & Orders"
45
+ },
46
+ "needs_text": false,
47
+ "confidence": 0.99,
48
+ "operation_probabilities": {"CLICK": 1.0, "TYPE_TEXT": 0.0, "WAIT": 0.0, "DONE": 0.0, "SCROLL_DOWN": 0.0, "BLOCKED": 0.0},
49
+ "target_probabilities": {"13": 0.95, "11": 0.03, "12": 0.02},
50
+ "elements": [{"index": "13", "ref": "e30", "selector": "#nav-orders", "role": "link", "label": "Returns & Orders", "operations": ["CLICK"]}]
51
+ }
52
+ ```
53
+
54
+ That response is real, not illustrative: the captured `ziniao-cli page snapshot` of a
55
+ public amazon.com page (`tests/fixtures/ziniao_page_snapshot.json`) with the goal *"Open
56
+ the orders page"* — `jev-1.13.0`, 2724 input tokens, 935 ms. The fixture ships in this
57
+ repo, so the shape above is reproducible offline.
58
+
59
+ - `--observation FILE` reads a file instead of stdin; `--history FILE` passes previous
60
+ steps as a JSON list; `--compact` emits one line.
61
+ - stdout is **always** JSON, including failures, so a caller parses one stream.
62
+ - Exit codes: `0` a decision, `2` the observation was unusable, `3` the model or its
63
+ credential failed.
64
+
65
+ Every response carries the whole `elements` index table with a `ref` and a `selector` for
66
+ each target, so the caller can execute or re-check without re-snapshotting.
67
+
68
+ ### Execute the decision
69
+
70
+ | Decision | How the caller runs it |
71
+ | --- | --- |
72
+ | `CLICK` | `ziniao-cli page click --store-id "$STORE" --ref <ref>` (or `--selector`) |
73
+ | `TYPE_TEXT` | the caller writes the value, then `ziniao-cli page input --store-id "$STORE" --selector <selector> --text <value> --clear` |
74
+ | `SELECT` | set the value at the returned `selector` (`page input` has no `--ref`) |
75
+ | `SCROLL_DOWN` / `SCROLL_UP` | `ziniao-cli page scroll --store-id "$STORE" --y ±560` |
76
+ | `WAIT` | observe again without acting |
77
+ | `DONE` / `BLOCKED` | stop. `DONE` is the model's choice, not proof: verify the outcome yourself |
78
+
79
+ **TYPE_TEXT never comes with text.** The service names the field and stops; the calling
80
+ agent supplies the value from its own context. That is why this package needs no text
81
+ model and no `TEXT_MODEL_API_KEY`.
82
+
83
+ ## The observation
84
+
85
+ `page snapshot` is the only `ziniao-cli` page command that returns an indexed element
86
+ table with both semantic identity (role, accessible name, context, geometry) **and**
87
+ actionable locators. `page content --content-format structured` reads better but its
88
+ `selectorHint` is often just a tag name and form controls carry no locator, so it cannot
89
+ drive a choice.
90
+
91
+ The current `ziniao-page` skill draws the same line from the other side: it treats
92
+ `page snapshot` as a local fallback — for upload controls, custom or icon-only controls,
93
+ and candidates that survive `page query` and open-shadow-DOM checks — and prefers
94
+ `page content --content-format structured` for *understanding* a page. That preference is
95
+ about reading, not about choosing: only the snapshot carries a `ref` and a real
96
+ `selector`, so a decision still needs one. Take a snapshot when a decision is due; do not
97
+ turn it into a page poll.
98
+
99
+ - Supply `--max-items` generously. The cap is the observation, so a low cap hides targets —
100
+ and a small value is not a reliable way to keep the observation short: measured against a
101
+ live store, `--max-items 3` and `--max-items 5` both returned 10 elements while `20`
102
+ returned 20.
103
+ - A `ref` belongs to one store, tab, and page, and expires on reload or any URL change.
104
+ Re-snapshot after a navigation instead of reusing `ref`; `--target-id` picks the tab in a
105
+ multi-tab store.
106
+ - Off-canvas elements are dropped the same way the browser-side reader drops them. An
107
+ element hidden with `left:-9999px` has real width and height, so geometry is the only
108
+ signal; without it, "skip to content" links become targets.
109
+ - A `<select>` is offered as a dropdown to open, because the snapshot names it without
110
+ listing its options. Add an `options` array to that element in a normalized observation
111
+ to get the `SELECT` operation.
112
+ - A page snapshot carries no scroll position, so the service offers scrolling down only.
113
+ Add `scroll: {"y":…, "height":…, "viewport":…}` to make both directions exact.
114
+ - Page text is synthesized from the element table when the caller supplies none; merge a
115
+ `text` field into the payload, or send a normalized observation, for real page prose.
116
+
117
+ ## Credentials
118
+
119
+ `TYPESAFE_API_KEY` is required. The process environment wins, then files fill in what is
120
+ missing, most specific first:
121
+
122
+ | Order | Location |
123
+ | --- | --- |
124
+ | 1 | the process environment |
125
+ | 2 | `./.env` |
126
+ | 3 | `~/.config/jev/.env` (or `$XDG_CONFIG_HOME/jev/.env`) |
127
+
128
+ The third location exists so a CLI started by a daemon, a GUI, or another agent finds its
129
+ credentials without a shell rc file — **an agent process does not read `~/.bashrc`**.
130
+ A missing key needs no separate check command: `decide` answers with exit `3` and a
131
+ parseable `{"error":{"type":"credential"}}`.
132
+
133
+ The decision endpoint is configuration, not a credential, but it resolves through the same
134
+ three locations: `TYPESAFE_BASE_URL` overrides the default
135
+ `https://api.typesafe.ai/v1/systemone`, and an empty value means the default. `doctor`
136
+ reports the endpoint it would use in both the `credentials` and `model` checks.
137
+
138
+ ```bash
139
+ mkdir -p ~/.config/jev && cp .env.example ~/.config/jev/.env
140
+ ```
141
+
142
+ ## Doctor
143
+
144
+ ```bash
145
+ jev doctor
146
+ ```
147
+
148
+ One read-only self-check for the whole install, and the fastest way to answer "why did
149
+ `decide` exit 3?":
150
+
151
+ | Check | What it reports |
152
+ | --- | --- |
153
+ | `runtime` | the interpreter actually running, and the version this package requires |
154
+ | `install` | the version, and whether this is the editable checkout or a frozen install |
155
+ | `credentials` | **where** the key came from (process environment, `./.env`, `~/.config/jev/.env`) and **how long** it is — never the key itself |
156
+ | `model` | one minimal model request that proves the service answers |
157
+
158
+ The last check is a real request on purpose: an unreachable service and a wrong decision
159
+ look identical from the caller's side, so `doctor` asks a question, names the answer it
160
+ expects, and fails if something else comes back — a reachable endpoint answering with the
161
+ wrong model is a failure, not a pass. The request carries no page state and no goal and
162
+ costs roughly 400 input tokens.
163
+
164
+ The same run, one line per check, detail trimmed:
165
+
166
+ ```json
167
+ {"status": "ok", "command": "doctor", "checks": [
168
+ {"name": "runtime", "status": "ok", "detail": {"python": "3.13.12", "required": "3.12"}},
169
+ {"name": "install", "status": "ok", "detail": {"version": "0.1.0", "mode": "editable"}},
170
+ {"name": "credentials", "status": "ok", "detail": {"source": "~/.config/jev/.env", "key": {"present": true, "length": 108}, "endpoint": "https://api.typesafe.ai/v1/systemone"}},
171
+ {"name": "model", "status": "ok", "detail": {"endpoint": "https://api.typesafe.ai/v1/systemone", "answer": "PONG", "model": "jev-1.13.0", "latency_ms": 1025}}
172
+ ], "summary": {"passed": 4, "failed": 0, "skipped": 0}}
173
+ ```
174
+
175
+ Exit `0` when every check passes, `3` when this install cannot decide. Same rule as
176
+ `decide`: stdout is always one parseable JSON document, and `--compact` emits it on one
177
+ line. `doctor` never reads an observation, so it needs no stdin.
178
+
179
+ ## Development
180
+
181
+ ```bash
182
+ uv run ruff check .
183
+ uv run pytest
184
+ uv build
185
+ ```
186
+
187
+ Tests are offline: they use a captured page snapshot and a fake model call. Nothing in
188
+ `pytest` reaches the network, opens a browser, or spends money.
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "jev-decide"
3
+ version = "0.1.0"
4
+ description = "The jev decision service: page state in, one typed operation and target out."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.12"
8
+ dependencies = ["httpx[http2]>=0.28,<1"]
9
+
10
+ [project.scripts]
11
+ jev = "jev_decide.cli:main"
12
+
13
+ [build-system]
14
+ requires = ["hatchling"]
15
+ build-backend = "hatchling.build"
16
+
17
+ [dependency-groups]
18
+ dev = ["pytest>=8.4,<9", "ruff>=0.14,<1"]
19
+
20
+ [tool.hatch.build.targets.wheel]
21
+ packages = ["src/jev_decide"]
22
+
23
+ [tool.ruff]
24
+ line-length = 120
25
+
26
+ [tool.ruff.lint]
27
+ select = ["E", "F", "I"]
28
+
29
+ [tool.pytest.ini_options]
30
+ testpaths = ["tests"]
@@ -0,0 +1,5 @@
1
+ """The jev decision service: page state in, one typed operation and target out.
2
+
3
+ `jev decide` observes nothing, executes nothing, and never generates text. The caller
4
+ drives the browser and executes the operation it returns.
5
+ """