webbee 0.2.0__tar.gz → 0.2.2__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.
- {webbee-0.2.0 → webbee-0.2.2}/CHANGELOG.md +19 -0
- {webbee-0.2.0 → webbee-0.2.2}/PKG-INFO +1 -1
- {webbee-0.2.0 → webbee-0.2.2}/pyproject.toml +1 -1
- webbee-0.2.2/src/webbee/__init__.py +1 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/render.py +7 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/session.py +18 -1
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_marathon.py +69 -8
- webbee-0.2.0/src/webbee/__init__.py +0 -1
- {webbee-0.2.0 → webbee-0.2.2}/.github/workflows/publish.yml +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/.gitignore +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/LICENSE +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/README.md +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/install.sh +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/account.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/banner_art.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/cli.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/clipboard.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/commands.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/config.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/details.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/events.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/frames.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/__init__.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/chunker.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/embed.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/graph.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/indexer.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/models.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/query.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/service.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/store.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/vectors.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/intel/watch.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/output_pane.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/repl.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/repo.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/sessions.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/stream.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/tools.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/tui.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/src/webbee/update.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/__init__.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_account.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_cli.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_clipboard.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_commands.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_config.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_cpc_contract_stable.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_details.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_events.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_freeze_fix.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_intel_chunker.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_intel_embed.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_intel_graph.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_intel_indexer.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_intel_no_numpy.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_intel_query.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_intel_service.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_intel_store.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_intel_vectors.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_intel_watch.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_packaging.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_render.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_repl.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_repo.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_session.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_sessions.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_stream.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_tools.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_tui.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_tui_hardening.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_update.py +0 -0
- {webbee-0.2.0 → webbee-0.2.2}/tests/test_version.py +0 -0
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
- **See Webbee's reasoning as a distinct 💭 block.** Before each step, Webbee now
|
|
6
|
+
shows a genuine line of thinking — what it's weighing and why — rendered as its
|
|
7
|
+
own 💭 block, separate from status lines. Works on every model.
|
|
8
|
+
- **SSH steps are visible.** When Webbee runs a command on one of your connected
|
|
9
|
+
servers, the terminal now shows that step (command + result), like any other.
|
|
10
|
+
|
|
11
|
+
## 0.2.1
|
|
12
|
+
|
|
13
|
+
- **Fixes a marathon that could hang on "working" and quietly burn credits.** A
|
|
14
|
+
marathon works through milestones and streams a result at each one; the
|
|
15
|
+
terminal used to stop at the first milestone, abandoning the rest of the run —
|
|
16
|
+
the agent kept going server-side with nobody to run its tools, spinning until
|
|
17
|
+
your balance drained. The terminal now follows a marathon to the end: it keeps
|
|
18
|
+
working across milestones and stops only when the whole goal is done, it
|
|
19
|
+
pauses (low credits / awaiting approval), or you stop it. Requires the current
|
|
20
|
+
cloud; older milestones are unaffected.
|
|
21
|
+
|
|
3
22
|
## 0.2.0
|
|
4
23
|
|
|
5
24
|
- **Marathon is now the default.** A task you type runs to completion on its own
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.2"
|
|
@@ -325,6 +325,13 @@ class RichSink:
|
|
|
325
325
|
self.console.print(Text(" " + text, style="dim italic"))
|
|
326
326
|
self._nudge()
|
|
327
327
|
|
|
328
|
+
def thinking(self, text: str) -> None:
|
|
329
|
+
# System-driven reasoning as a distinct 💭 block — visually apart from the
|
|
330
|
+
# dim `progress` line (which stays reserved for status like low-balance).
|
|
331
|
+
if text:
|
|
332
|
+
self.console.print(Text(" 💭 " + text, style="italic medium_purple3"))
|
|
333
|
+
self._nudge()
|
|
334
|
+
|
|
328
335
|
def plan_blocked(self, tool: str) -> None:
|
|
329
336
|
"""Plan mode auto-declines writes/destructive. Tell the user WHY and how
|
|
330
337
|
to allow it (Shift+Tab). Autopilot and default never reach this."""
|
|
@@ -219,7 +219,7 @@ class AgentSession:
|
|
|
219
219
|
# persistent stream (task_id absent on legacy kernels -> not filtered).
|
|
220
220
|
if _ftid and self._task_id and _ftid != self._task_id and ftype in (
|
|
221
221
|
"tool_request", "confirm_request", "final",
|
|
222
|
-
"panel_release_required"):
|
|
222
|
+
"marathon_complete", "panel_release_required"):
|
|
223
223
|
continue
|
|
224
224
|
|
|
225
225
|
if ftype == "tool_request":
|
|
@@ -272,6 +272,9 @@ class AgentSession:
|
|
|
272
272
|
elif ftype == "step_finished": # v2 (Slice-5 T8 dual-emit)
|
|
273
273
|
handle_step_finished(frame, sink, finished, step_labels, self.steps, local_ids)
|
|
274
274
|
|
|
275
|
+
elif ftype == "thinking": # system-driven reasoning -> the 💭 block
|
|
276
|
+
(getattr(sink, "thinking", None) or sink.progress)(_progress_text(frame))
|
|
277
|
+
|
|
275
278
|
elif ftype == "progress": # P2 — dual-reads llm_text (v2) / text (legacy)
|
|
276
279
|
sink.progress(_progress_text(frame))
|
|
277
280
|
|
|
@@ -281,6 +284,9 @@ class AgentSession:
|
|
|
281
284
|
int(frame.get("credits", 0) or 0),
|
|
282
285
|
)
|
|
283
286
|
|
|
287
|
+
elif ftype == "marathon_complete": # U4 — the whole GOAL is done: terminal
|
|
288
|
+
return frame.get("text", "")
|
|
289
|
+
|
|
284
290
|
elif ftype in _MARATHON_FACT_TYPES: # U4 — marathon plan/milestone/pause
|
|
285
291
|
# Facts-only; render ONE human-readable line. Guarded: a
|
|
286
292
|
# minimal sink (no `note`) simply drops the fact rather than
|
|
@@ -289,8 +295,19 @@ class AgentSession:
|
|
|
289
295
|
_note = getattr(sink, "note", None)
|
|
290
296
|
if _note is not None:
|
|
291
297
|
_note(marathon_note(frame))
|
|
298
|
+
if ftype == "marathon_paused":
|
|
299
|
+
# Parked (out-of-credits / consent / runaway) -> end the
|
|
300
|
+
# turn so the dock leaves "working"; the note shows why, and
|
|
301
|
+
# the run resumes on the user's next reply.
|
|
302
|
+
return ""
|
|
292
303
|
|
|
293
304
|
elif ftype == "final":
|
|
305
|
+
# In a MARATHON a `final` is a PER-MILESTONE result, NOT the end
|
|
306
|
+
# of the run -> keep streaming (the goal ends on marathon_complete
|
|
307
|
+
# / marathon_paused, or a user-stop `final` with stopped=true). A
|
|
308
|
+
# non-marathon coding turn's `final` is terminal (unchanged).
|
|
309
|
+
if marathon and not frame.get("stopped"):
|
|
310
|
+
continue
|
|
294
311
|
return frame.get("text", "")
|
|
295
312
|
|
|
296
313
|
return ""
|
|
@@ -85,9 +85,9 @@ def test_parser_marathon_defaults_none():
|
|
|
85
85
|
assert build_parser().parse_args([]).marathon is None
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
def _run_marathon_capture_post(monkeypatch, tmp_path, goal="build X"):
|
|
89
|
-
"""Drive AgentSession.run(
|
|
90
|
-
|
|
88
|
+
def _run_marathon_capture_post(monkeypatch, tmp_path, goal="build X", frames=None, marathon=True):
|
|
89
|
+
"""Drive AgentSession.run() with everything network-side faked; return the
|
|
90
|
+
JSON body POSTed to /v1/agent/sessions, the run() result, and the sink."""
|
|
91
91
|
import httpx
|
|
92
92
|
import imperal_mcp.client as ic
|
|
93
93
|
import webbee.session as S
|
|
@@ -113,11 +113,18 @@ def _run_marathon_capture_post(monkeypatch, tmp_path, goal="build X"):
|
|
|
113
113
|
|
|
114
114
|
monkeypatch.setattr(T, "LocalToolExecutor", _RecExecutor)
|
|
115
115
|
|
|
116
|
+
# Default protocol (U4 semantics): a marathon_plan FACT + a per-milestone
|
|
117
|
+
# `final` (NON-terminal) + the terminal marathon_complete. Tests override
|
|
118
|
+
# `frames` to exercise the terminal taxonomy.
|
|
119
|
+
_frames = frames if frames is not None else [
|
|
120
|
+
{"type": "marathon_plan", "task_id": "OURS", "milestone_count": 2, "goal": goal},
|
|
121
|
+
{"type": "final", "task_id": "OURS", "text": "milestone 1"},
|
|
122
|
+
{"type": "marathon_complete", "task_id": "OURS", "text": "done"},
|
|
123
|
+
]
|
|
124
|
+
|
|
116
125
|
async def _fake_stream(client, session_id, headers_provider, *, start_id="0-0"):
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"goal": goal}
|
|
120
|
-
yield {"type": "final", "task_id": "OURS", "text": "done"}
|
|
126
|
+
for _fr in _frames:
|
|
127
|
+
yield _fr
|
|
121
128
|
|
|
122
129
|
monkeypatch.setattr(ST, "stream_frames", _fake_stream)
|
|
123
130
|
|
|
@@ -147,6 +154,7 @@ def _run_marathon_capture_post(monkeypatch, tmp_path, goal="build X"):
|
|
|
147
154
|
class NoteSink:
|
|
148
155
|
def __init__(self): self.notes = []
|
|
149
156
|
def note(self, msg): self.notes.append(msg)
|
|
157
|
+
def thinking(self, msg): self.thinks = getattr(self, "thinks", []) + [msg]
|
|
150
158
|
def tool_start(self, *a): ...
|
|
151
159
|
def tool_result(self, *a): ...
|
|
152
160
|
def ask_consent(self, *a): return "y"
|
|
@@ -161,7 +169,7 @@ def _run_marathon_capture_post(monkeypatch, tmp_path, goal="build X"):
|
|
|
161
169
|
|
|
162
170
|
sink = NoteSink()
|
|
163
171
|
sess = S.AgentSession(cfg=_Cfg(), token_provider=token_provider, workspace_root=str(tmp_path))
|
|
164
|
-
result = asyncio.run(sess.run(goal, sink, marathon=
|
|
172
|
+
result = asyncio.run(sess.run(goal, sink, marathon=marathon, goal=(goal if marathon else "")))
|
|
165
173
|
return posted, result, sink
|
|
166
174
|
|
|
167
175
|
|
|
@@ -176,6 +184,59 @@ def test_marathon_run_sets_marathon_and_goal_and_verify_cmd(monkeypatch, tmp_pat
|
|
|
176
184
|
assert any("Marathon plan" in n for n in sink.notes)
|
|
177
185
|
|
|
178
186
|
|
|
187
|
+
def test_marathon_final_is_not_terminal_keeps_streaming(monkeypatch, tmp_path):
|
|
188
|
+
# A per-milestone `final` must NOT end a marathon run: with no terminal frame
|
|
189
|
+
# the stream simply exhausts and run() returns "" (it kept streaming past the
|
|
190
|
+
# milestone final, never bailed early — the phantom-hang root cause).
|
|
191
|
+
_, result, _ = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
|
|
192
|
+
{"type": "marathon_plan", "task_id": "OURS", "goal": "g"},
|
|
193
|
+
{"type": "final", "task_id": "OURS", "text": "milestone 1 done"},
|
|
194
|
+
])
|
|
195
|
+
assert result == ""
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_marathon_complete_is_terminal(monkeypatch, tmp_path):
|
|
199
|
+
_, result, _ = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
|
|
200
|
+
{"type": "final", "task_id": "OURS", "text": "milestone 1"},
|
|
201
|
+
{"type": "marathon_complete", "task_id": "OURS", "text": "goal done"},
|
|
202
|
+
])
|
|
203
|
+
assert result == "goal done"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_thinking_frame_renders_as_distinct_channel(monkeypatch, tmp_path):
|
|
207
|
+
# A `thinking` frame routes to the distinct 💭 reasoning channel
|
|
208
|
+
# (sink.thinking), NOT the plain progress/note lines.
|
|
209
|
+
_, _, sink = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
|
|
210
|
+
{"type": "thinking", "task_id": "OURS", "text": "assessing the repo before editing"},
|
|
211
|
+
{"type": "marathon_complete", "task_id": "OURS", "text": "done"},
|
|
212
|
+
])
|
|
213
|
+
assert getattr(sink, "thinks", []) == ["assessing the repo before editing"]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_marathon_paused_is_terminal_and_noted(monkeypatch, tmp_path):
|
|
217
|
+
_, result, sink = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
|
|
218
|
+
{"type": "marathon_paused", "task_id": "OURS", "reason": "out of credits"},
|
|
219
|
+
])
|
|
220
|
+
assert result == ""
|
|
221
|
+
assert any("paused" in n.lower() for n in sink.notes)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def test_marathon_user_stop_final_is_terminal(monkeypatch, tmp_path):
|
|
225
|
+
# A user-stop (stopped=true) `final` ends the marathon immediately.
|
|
226
|
+
_, result, _ = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
|
|
227
|
+
{"type": "final", "task_id": "OURS", "text": "partial", "stopped": True},
|
|
228
|
+
])
|
|
229
|
+
assert result == "partial"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_non_marathon_final_is_terminal(monkeypatch, tmp_path):
|
|
233
|
+
# Coding (non-marathon): a `final` is terminal (unchanged) -> returns its text.
|
|
234
|
+
_, result, _ = _run_marathon_capture_post(monkeypatch, tmp_path, marathon=False, frames=[
|
|
235
|
+
{"type": "final", "task_id": "OURS", "text": "answer"},
|
|
236
|
+
])
|
|
237
|
+
assert result == "answer"
|
|
238
|
+
|
|
239
|
+
|
|
179
240
|
def test_normal_run_omits_marathon_fields(monkeypatch, tmp_path):
|
|
180
241
|
# Coding path unchanged: no marathon/goal keys, no verify_cmd injected.
|
|
181
242
|
import httpx
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|