webbee 0.2.0__tar.gz → 0.2.1__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.
Files changed (73) hide show
  1. {webbee-0.2.0 → webbee-0.2.1}/CHANGELOG.md +11 -0
  2. {webbee-0.2.0 → webbee-0.2.1}/PKG-INFO +1 -1
  3. {webbee-0.2.0 → webbee-0.2.1}/pyproject.toml +1 -1
  4. webbee-0.2.1/src/webbee/__init__.py +1 -0
  5. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/session.py +15 -1
  6. {webbee-0.2.0 → webbee-0.2.1}/tests/test_marathon.py +58 -8
  7. webbee-0.2.0/src/webbee/__init__.py +0 -1
  8. {webbee-0.2.0 → webbee-0.2.1}/.github/workflows/publish.yml +0 -0
  9. {webbee-0.2.0 → webbee-0.2.1}/.gitignore +0 -0
  10. {webbee-0.2.0 → webbee-0.2.1}/LICENSE +0 -0
  11. {webbee-0.2.0 → webbee-0.2.1}/README.md +0 -0
  12. {webbee-0.2.0 → webbee-0.2.1}/install.sh +0 -0
  13. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/account.py +0 -0
  14. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/banner_art.py +0 -0
  15. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/cli.py +0 -0
  16. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/clipboard.py +0 -0
  17. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/commands.py +0 -0
  18. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/config.py +0 -0
  19. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/details.py +0 -0
  20. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/events.py +0 -0
  21. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/frames.py +0 -0
  22. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/__init__.py +0 -0
  23. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/chunker.py +0 -0
  24. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/embed.py +0 -0
  25. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/graph.py +0 -0
  26. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/indexer.py +0 -0
  27. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/models.py +0 -0
  28. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/query.py +0 -0
  29. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/service.py +0 -0
  30. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/store.py +0 -0
  31. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/vectors.py +0 -0
  32. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/intel/watch.py +0 -0
  33. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/output_pane.py +0 -0
  34. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/render.py +0 -0
  35. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/repl.py +0 -0
  36. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/repo.py +0 -0
  37. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/sessions.py +0 -0
  38. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/stream.py +0 -0
  39. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/tools.py +0 -0
  40. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/tui.py +0 -0
  41. {webbee-0.2.0 → webbee-0.2.1}/src/webbee/update.py +0 -0
  42. {webbee-0.2.0 → webbee-0.2.1}/tests/__init__.py +0 -0
  43. {webbee-0.2.0 → webbee-0.2.1}/tests/test_account.py +0 -0
  44. {webbee-0.2.0 → webbee-0.2.1}/tests/test_cli.py +0 -0
  45. {webbee-0.2.0 → webbee-0.2.1}/tests/test_clipboard.py +0 -0
  46. {webbee-0.2.0 → webbee-0.2.1}/tests/test_commands.py +0 -0
  47. {webbee-0.2.0 → webbee-0.2.1}/tests/test_config.py +0 -0
  48. {webbee-0.2.0 → webbee-0.2.1}/tests/test_cpc_contract_stable.py +0 -0
  49. {webbee-0.2.0 → webbee-0.2.1}/tests/test_details.py +0 -0
  50. {webbee-0.2.0 → webbee-0.2.1}/tests/test_events.py +0 -0
  51. {webbee-0.2.0 → webbee-0.2.1}/tests/test_freeze_fix.py +0 -0
  52. {webbee-0.2.0 → webbee-0.2.1}/tests/test_intel_chunker.py +0 -0
  53. {webbee-0.2.0 → webbee-0.2.1}/tests/test_intel_embed.py +0 -0
  54. {webbee-0.2.0 → webbee-0.2.1}/tests/test_intel_graph.py +0 -0
  55. {webbee-0.2.0 → webbee-0.2.1}/tests/test_intel_indexer.py +0 -0
  56. {webbee-0.2.0 → webbee-0.2.1}/tests/test_intel_no_numpy.py +0 -0
  57. {webbee-0.2.0 → webbee-0.2.1}/tests/test_intel_query.py +0 -0
  58. {webbee-0.2.0 → webbee-0.2.1}/tests/test_intel_service.py +0 -0
  59. {webbee-0.2.0 → webbee-0.2.1}/tests/test_intel_store.py +0 -0
  60. {webbee-0.2.0 → webbee-0.2.1}/tests/test_intel_vectors.py +0 -0
  61. {webbee-0.2.0 → webbee-0.2.1}/tests/test_intel_watch.py +0 -0
  62. {webbee-0.2.0 → webbee-0.2.1}/tests/test_packaging.py +0 -0
  63. {webbee-0.2.0 → webbee-0.2.1}/tests/test_render.py +0 -0
  64. {webbee-0.2.0 → webbee-0.2.1}/tests/test_repl.py +0 -0
  65. {webbee-0.2.0 → webbee-0.2.1}/tests/test_repo.py +0 -0
  66. {webbee-0.2.0 → webbee-0.2.1}/tests/test_session.py +0 -0
  67. {webbee-0.2.0 → webbee-0.2.1}/tests/test_sessions.py +0 -0
  68. {webbee-0.2.0 → webbee-0.2.1}/tests/test_stream.py +0 -0
  69. {webbee-0.2.0 → webbee-0.2.1}/tests/test_tools.py +0 -0
  70. {webbee-0.2.0 → webbee-0.2.1}/tests/test_tui.py +0 -0
  71. {webbee-0.2.0 → webbee-0.2.1}/tests/test_tui_hardening.py +0 -0
  72. {webbee-0.2.0 → webbee-0.2.1}/tests/test_update.py +0 -0
  73. {webbee-0.2.0 → webbee-0.2.1}/tests/test_version.py +0 -0
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1
4
+
5
+ - **Fixes a marathon that could hang on "working" and quietly burn credits.** A
6
+ marathon works through milestones and streams a result at each one; the
7
+ terminal used to stop at the first milestone, abandoning the rest of the run —
8
+ the agent kept going server-side with nobody to run its tools, spinning until
9
+ your balance drained. The terminal now follows a marathon to the end: it keeps
10
+ working across milestones and stops only when the whole goal is done, it
11
+ pauses (low credits / awaiting approval), or you stop it. Requires the current
12
+ cloud; older milestones are unaffected.
13
+
3
14
  ## 0.2.0
4
15
 
5
16
  - **Marathon is now the default.** A task you type runs to completion on its own
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: webbee
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Webbee 🐝 — the Imperal Cloud coding agent in your terminal
5
5
  Project-URL: Homepage, https://imperal.io
6
6
  Project-URL: Documentation, https://docs.imperal.io
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "webbee"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Webbee 🐝 — the Imperal Cloud coding agent in your terminal"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -0,0 +1 @@
1
+ __version__ = "0.2.1"
@@ -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":
@@ -281,6 +281,9 @@ class AgentSession:
281
281
  int(frame.get("credits", 0) or 0),
282
282
  )
283
283
 
284
+ elif ftype == "marathon_complete": # U4 — the whole GOAL is done: terminal
285
+ return frame.get("text", "")
286
+
284
287
  elif ftype in _MARATHON_FACT_TYPES: # U4 — marathon plan/milestone/pause
285
288
  # Facts-only; render ONE human-readable line. Guarded: a
286
289
  # minimal sink (no `note`) simply drops the fact rather than
@@ -289,8 +292,19 @@ class AgentSession:
289
292
  _note = getattr(sink, "note", None)
290
293
  if _note is not None:
291
294
  _note(marathon_note(frame))
295
+ if ftype == "marathon_paused":
296
+ # Parked (out-of-credits / consent / runaway) -> end the
297
+ # turn so the dock leaves "working"; the note shows why, and
298
+ # the run resumes on the user's next reply.
299
+ return ""
292
300
 
293
301
  elif ftype == "final":
302
+ # In a MARATHON a `final` is a PER-MILESTONE result, NOT the end
303
+ # of the run -> keep streaming (the goal ends on marathon_complete
304
+ # / marathon_paused, or a user-stop `final` with stopped=true). A
305
+ # non-marathon coding turn's `final` is terminal (unchanged).
306
+ if marathon and not frame.get("stopped"):
307
+ continue
294
308
  return frame.get("text", "")
295
309
 
296
310
  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(marathon=True) with everything network-side faked;
90
- return the JSON body POSTed to /v1/agent/sessions."""
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
- # Include a marathon FACT frame to exercise the render branch.
118
- yield {"type": "marathon_plan", "task_id": "OURS", "milestone_count": 2,
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
 
@@ -161,7 +168,7 @@ def _run_marathon_capture_post(monkeypatch, tmp_path, goal="build X"):
161
168
 
162
169
  sink = NoteSink()
163
170
  sess = S.AgentSession(cfg=_Cfg(), token_provider=token_provider, workspace_root=str(tmp_path))
164
- result = asyncio.run(sess.run(goal, sink, marathon=True, goal=goal))
171
+ result = asyncio.run(sess.run(goal, sink, marathon=marathon, goal=(goal if marathon else "")))
165
172
  return posted, result, sink
166
173
 
167
174
 
@@ -176,6 +183,49 @@ def test_marathon_run_sets_marathon_and_goal_and_verify_cmd(monkeypatch, tmp_pat
176
183
  assert any("Marathon plan" in n for n in sink.notes)
177
184
 
178
185
 
186
+ def test_marathon_final_is_not_terminal_keeps_streaming(monkeypatch, tmp_path):
187
+ # A per-milestone `final` must NOT end a marathon run: with no terminal frame
188
+ # the stream simply exhausts and run() returns "" (it kept streaming past the
189
+ # milestone final, never bailed early — the phantom-hang root cause).
190
+ _, result, _ = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
191
+ {"type": "marathon_plan", "task_id": "OURS", "goal": "g"},
192
+ {"type": "final", "task_id": "OURS", "text": "milestone 1 done"},
193
+ ])
194
+ assert result == ""
195
+
196
+
197
+ def test_marathon_complete_is_terminal(monkeypatch, tmp_path):
198
+ _, result, _ = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
199
+ {"type": "final", "task_id": "OURS", "text": "milestone 1"},
200
+ {"type": "marathon_complete", "task_id": "OURS", "text": "goal done"},
201
+ ])
202
+ assert result == "goal done"
203
+
204
+
205
+ def test_marathon_paused_is_terminal_and_noted(monkeypatch, tmp_path):
206
+ _, result, sink = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
207
+ {"type": "marathon_paused", "task_id": "OURS", "reason": "out of credits"},
208
+ ])
209
+ assert result == ""
210
+ assert any("paused" in n.lower() for n in sink.notes)
211
+
212
+
213
+ def test_marathon_user_stop_final_is_terminal(monkeypatch, tmp_path):
214
+ # A user-stop (stopped=true) `final` ends the marathon immediately.
215
+ _, result, _ = _run_marathon_capture_post(monkeypatch, tmp_path, frames=[
216
+ {"type": "final", "task_id": "OURS", "text": "partial", "stopped": True},
217
+ ])
218
+ assert result == "partial"
219
+
220
+
221
+ def test_non_marathon_final_is_terminal(monkeypatch, tmp_path):
222
+ # Coding (non-marathon): a `final` is terminal (unchanged) -> returns its text.
223
+ _, result, _ = _run_marathon_capture_post(monkeypatch, tmp_path, marathon=False, frames=[
224
+ {"type": "final", "task_id": "OURS", "text": "answer"},
225
+ ])
226
+ assert result == "answer"
227
+
228
+
179
229
  def test_normal_run_omits_marathon_fields(monkeypatch, tmp_path):
180
230
  # Coding path unchanged: no marathon/goal keys, no verify_cmd injected.
181
231
  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