@widgex/wgx 0.0.0-stage → 0.4.2
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.
- package/LICENSE.txt +49 -0
- package/README.md +33 -2
- package/ThirdPartyNotices.txt +4685 -0
- package/package.json +30 -4
- package/provider-clis.json +1211 -0
- package/python/XACTOR.md +146 -0
- package/python/browser_actor_host.py +456 -0
- package/python/cdp_page.py +363 -0
- package/python/harness_check.py +77 -0
- package/python/requirements.txt +1 -0
- package/python/test_line_endings.py +185 -0
- package/python/xactor/__init__.py +18 -0
- package/python/xactor/__main__.py +226 -0
- package/python/xactor/api.py +882 -0
- package/python/xactor/bridge.py +784 -0
- package/python/xactor/dom.py +117 -0
- package/python/xactor/man/add_card.md +89 -0
- package/python/xactor/man/agent-vs-fork.md +108 -0
- package/python/xactor/man/agent.md +333 -0
- package/python/xactor/man/ask_model.md +110 -0
- package/python/xactor/man/ask_reference.md +56 -0
- package/python/xactor/man/ask_user.md +103 -0
- package/python/xactor/man/attach_app.md +595 -0
- package/python/xactor/man/attach_ide.md +65 -0
- package/python/xactor/man/attach_ide_surface.md +31 -0
- package/python/xactor/man/await_user.md +214 -0
- package/python/xactor/man/bash.md +142 -0
- package/python/xactor/man/browser-driver.md +137 -0
- package/python/xactor/man/browser_activity.md +53 -0
- package/python/xactor/man/browser_attach.md +84 -0
- package/python/xactor/man/browser_close_tab.md +50 -0
- package/python/xactor/man/browser_detach.md +50 -0
- package/python/xactor/man/browser_exec.md +66 -0
- package/python/xactor/man/browser_focus_tab.md +47 -0
- package/python/xactor/man/browser_hybrid.md +47 -0
- package/python/xactor/man/browser_open.md +50 -0
- package/python/xactor/man/browser_open_tab.md +58 -0
- package/python/xactor/man/browser_plumbing.md +107 -0
- package/python/xactor/man/browser_screenshot.md +56 -0
- package/python/xactor/man/browser_snapshot.md +52 -0
- package/python/xactor/man/browser_tabs.md +67 -0
- package/python/xactor/man/bulletin.md +88 -0
- package/python/xactor/man/capture_page.md +54 -0
- package/python/xactor/man/cd.md +64 -0
- package/python/xactor/man/chess_move.md +53 -0
- package/python/xactor/man/children.md +89 -0
- package/python/xactor/man/close_task.md +14 -0
- package/python/xactor/man/compact.md +75 -0
- package/python/xactor/man/drive.md +140 -0
- package/python/xactor/man/drive_space.md +72 -0
- package/python/xactor/man/driving.md +168 -0
- package/python/xactor/man/edit_file.md +112 -0
- package/python/xactor/man/finding.md +194 -0
- package/python/xactor/man/fork_agent.md +45 -0
- package/python/xactor/man/glob.md +83 -0
- package/python/xactor/man/grep.md +115 -0
- package/python/xactor/man/harness.md +165 -0
- package/python/xactor/man/harness_check.md +70 -0
- package/python/xactor/man/harness_format.md +257 -0
- package/python/xactor/man/highlight.md +66 -0
- package/python/xactor/man/instruct.md +111 -0
- package/python/xactor/man/ls.md +63 -0
- package/python/xactor/man/man.md +86 -0
- package/python/xactor/man/merge_wiz.md +40 -0
- package/python/xactor/man/notebook.md +98 -0
- package/python/xactor/man/overview.md +89 -0
- package/python/xactor/man/permissions.md +137 -0
- package/python/xactor/man/phase.md +150 -0
- package/python/xactor/man/powershell.md +74 -0
- package/python/xactor/man/profiles.md +60 -0
- package/python/xactor/man/project_drive.md +49 -0
- package/python/xactor/man/read_bulletin.md +89 -0
- package/python/xactor/man/read_file.md +90 -0
- package/python/xactor/man/relay.md +216 -0
- package/python/xactor/man/reply.md +72 -0
- package/python/xactor/man/reply_message.md +78 -0
- package/python/xactor/man/report_bug.md +118 -0
- package/python/xactor/man/request_answer.md +142 -0
- package/python/xactor/man/request_merge_decisions.md +121 -0
- package/python/xactor/man/request_operator_action.md +152 -0
- package/python/xactor/man/request_review.md +122 -0
- package/python/xactor/man/request_user_action.md +126 -0
- package/python/xactor/man/return_card.md +71 -0
- package/python/xactor/man/run.md +99 -0
- package/python/xactor/man/sandbox.md +144 -0
- package/python/xactor/man/session_fork.md +74 -0
- package/python/xactor/man/session_harness.md +109 -0
- package/python/xactor/man/session_join.md +102 -0
- package/python/xactor/man/session_land.md +73 -0
- package/python/xactor/man/session_nudge.md +73 -0
- package/python/xactor/man/session_rest.md +73 -0
- package/python/xactor/man/session_scope.md +84 -0
- package/python/xactor/man/session_terminate.md +58 -0
- package/python/xactor/man/session_token.md +78 -0
- package/python/xactor/man/sessions.md +93 -0
- package/python/xactor/man/set_harness.md +15 -0
- package/python/xactor/man/spawn_swarm.md +80 -0
- package/python/xactor/man/submit_evidence.md +54 -0
- package/python/xactor/man/swarm_brief.md +30 -0
- package/python/xactor/man/task_status.md +82 -0
- package/python/xactor/man/tasks.md +47 -0
- package/python/xactor/man/todo_write.md +104 -0
- package/python/xactor/man/update_card.md +70 -0
- package/python/xactor/man/use_profile.md +90 -0
- package/python/xactor/man/use_widget.md +82 -0
- package/python/xactor/man/wait_for_task.md +105 -0
- package/python/xactor/man/widget-model.md +284 -0
- package/python/xactor/man/widgets.md +59 -0
- package/python/xactor/man/write_file.md +76 -0
- package/python/xactor/man/writing-a-widget.md +142 -0
- package/python/xactor/man/xylon.md +120 -0
- package/python/xactor/man/xylon_blame.md +80 -0
- package/python/xactor/man/xylon_read.md +131 -0
- package/python/xactor/man/xylon_search.md +125 -0
- package/python/xactor/manual.py +195 -0
- package/python/xactor/parent_watch.py +115 -0
- package/python/xactor/plan_mode.py +350 -0
- package/python/xactor/sandbox.py +856 -0
- package/python/xactor/seals.py +438 -0
- package/python/xactor/shell_hints.py +210 -0
- package/python/xactor/states/__init__.py +13 -0
- package/python/xactor/states/base.py +523 -0
- package/python/xactor/states/registry.py +274 -0
- package/python/xactor/stdlib_api.py +6345 -0
- package/python/xactor/widgets/__init__.py +1 -0
- package/python/xactor/widgets/github_tokens.py +314 -0
- package/python/xactor/widgets/google_aistudio.py +365 -0
- package/settings-schema.json +403 -0
- package/viewer/index.css +160 -0
- package/viewer/index.js +15 -0
- package/wgx.mjs +290659 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Frozen DOMSnapshot dataclass — mirrors dom.ts:10-17."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class DOMSnapshot:
|
|
9
|
+
url: str
|
|
10
|
+
query: dict[str, list[str]]
|
|
11
|
+
cookies: dict[str, str]
|
|
12
|
+
tree: dict
|
|
13
|
+
raw_html: str
|
|
14
|
+
timestamp: float
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_dict(cls, d: dict) -> "DOMSnapshot":
|
|
18
|
+
return cls(
|
|
19
|
+
url=d.get("url", ""),
|
|
20
|
+
query=d.get("query", {}),
|
|
21
|
+
cookies=d.get("cookies", {}),
|
|
22
|
+
tree=d.get("tree", {}),
|
|
23
|
+
raw_html=d.get("rawHtml", d.get("raw_html", "")),
|
|
24
|
+
timestamp=d.get("timestamp", 0.0),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def text_content(self) -> str:
|
|
28
|
+
"""Extract all accessible name text from the AX tree for keyword matching.
|
|
29
|
+
|
|
30
|
+
Handles the CDP Accessibility.getFullAXTree format where ``name`` is
|
|
31
|
+
a dict like ``{"type": "computedString", "value": "..."}``.
|
|
32
|
+
"""
|
|
33
|
+
parts: list[str] = []
|
|
34
|
+
|
|
35
|
+
def walk(node: object) -> None:
|
|
36
|
+
if isinstance(node, dict):
|
|
37
|
+
name = node.get("name")
|
|
38
|
+
if isinstance(name, str) and name:
|
|
39
|
+
parts.append(name)
|
|
40
|
+
elif isinstance(name, dict):
|
|
41
|
+
val = name.get("value")
|
|
42
|
+
if isinstance(val, str) and val:
|
|
43
|
+
parts.append(val)
|
|
44
|
+
for v in node.values():
|
|
45
|
+
walk(v)
|
|
46
|
+
elif isinstance(node, list):
|
|
47
|
+
for item in node:
|
|
48
|
+
walk(item)
|
|
49
|
+
|
|
50
|
+
walk(self.tree)
|
|
51
|
+
return " ".join(parts)
|
|
52
|
+
|
|
53
|
+
def structural_hash(self) -> str:
|
|
54
|
+
"""Hash the DOM tree structure, ignoring text/value content."""
|
|
55
|
+
import hashlib, json
|
|
56
|
+
|
|
57
|
+
def strip_text(obj):
|
|
58
|
+
if isinstance(obj, dict):
|
|
59
|
+
return {k: strip_text(v) for k, v in obj.items() if k not in ('name', 'value', 'description')}
|
|
60
|
+
if isinstance(obj, list):
|
|
61
|
+
return [strip_text(i) for i in obj]
|
|
62
|
+
return obj
|
|
63
|
+
|
|
64
|
+
return hashlib.sha256(json.dumps(strip_text(self.tree), sort_keys=True).encode()).hexdigest()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ── Hybrid Semantic+Visual types ─────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(frozen=True)
|
|
71
|
+
class HybridNode:
|
|
72
|
+
"""A single interactive element with semantic + visual properties."""
|
|
73
|
+
id: str
|
|
74
|
+
role: str
|
|
75
|
+
name: str
|
|
76
|
+
value: Optional[str] = None
|
|
77
|
+
disabled: Optional[bool] = None
|
|
78
|
+
checked: Optional[Union[bool, str]] = None # True, False, or 'mixed'
|
|
79
|
+
expanded: Optional[bool] = None
|
|
80
|
+
visuals: dict = field(default_factory=dict) # color, bgColor, border, fontWeight, opacity
|
|
81
|
+
bounds: dict = field(default_factory=dict) # x, y, w, h
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, d: dict) -> "HybridNode":
|
|
85
|
+
return cls(
|
|
86
|
+
id=d.get("id", ""),
|
|
87
|
+
role=d.get("role", ""),
|
|
88
|
+
name=d.get("name", ""),
|
|
89
|
+
value=d.get("value"),
|
|
90
|
+
disabled=d.get("disabled"),
|
|
91
|
+
checked=d.get("checked"),
|
|
92
|
+
expanded=d.get("expanded"),
|
|
93
|
+
visuals=d.get("visuals", {}),
|
|
94
|
+
bounds=d.get("bounds", {}),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass(frozen=True)
|
|
99
|
+
class HybridDiff:
|
|
100
|
+
"""Diff between two hybrid snapshots — only what changed."""
|
|
101
|
+
url: dict # { before: str, after: str }
|
|
102
|
+
timestamp: float
|
|
103
|
+
added: list # list of HybridNode dicts
|
|
104
|
+
removed: list # list of { id, role, name }
|
|
105
|
+
changed: list # list of { id, role, name, changes }
|
|
106
|
+
unchanged_count: int = 0
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def from_dict(cls, d: dict) -> "HybridDiff":
|
|
110
|
+
return cls(
|
|
111
|
+
url=d.get("url", {}),
|
|
112
|
+
timestamp=d.get("timestamp", 0.0),
|
|
113
|
+
added=[HybridNode.from_dict(n) for n in d.get("added", [])],
|
|
114
|
+
removed=d.get("removed", []),
|
|
115
|
+
changed=d.get("changed", []),
|
|
116
|
+
unchanged_count=d.get("unchangedCount", d.get("unchanged_count", 0)),
|
|
117
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# api.add_card(prompt, title=None, backlog=False) -> dict
|
|
2
|
+
<!-- also: add_todo, todo_card, board_card, card, kanban, work-board -->
|
|
3
|
+
<!-- group: Talking to the user -->
|
|
4
|
+
|
|
5
|
+
Leave a card on the user's board. **Only when the user asks for one.** Not gated.
|
|
6
|
+
|
|
7
|
+
The board is the user's list, not yours. Finding something on the way past — a
|
|
8
|
+
rename left half-finished, a test that will break on the next refactor, a fix
|
|
9
|
+
that wants its own branch — is not a request for a card: say so in your reply
|
|
10
|
+
and leave it there. When they do ask ("put that on the board", "make a card for
|
|
11
|
+
it"), the card lands in Todo and waits until they have time for it.
|
|
12
|
+
|
|
13
|
+
## What lands
|
|
14
|
+
|
|
15
|
+
An ordinary card, sitting with the ones the user wrote themselves. They can drag
|
|
16
|
+
it into Doing and it spawns an agent on it exactly like any other; deleting it
|
|
17
|
+
is how they say no. There is no accept step and nothing is blocked on them.
|
|
18
|
+
|
|
19
|
+
The one difference is that it is **marked with your node** — a purple edge and a
|
|
20
|
+
`from B8` line — until they act on it. Clicking it, dragging it, editing it or
|
|
21
|
+
starting it clears the mark. So the colour means "you have not seen this yet",
|
|
22
|
+
not "an agent thought of it", and a board they come back to after a day shows at
|
|
23
|
+
a glance what is new.
|
|
24
|
+
|
|
25
|
+
## Options
|
|
26
|
+
|
|
27
|
+
- **`prompt`** — what should be done. This is sent **verbatim** to the agent if
|
|
28
|
+
the card is ever started, so write it as an instruction, not as an
|
|
29
|
+
observation: *"Rename the `daemon` field in .widgex/bulletin.json to
|
|
30
|
+
`harness`"*, not *"the bulletin still says daemon"*. Up to 8000 characters.
|
|
31
|
+
- **`title`** — what the card is called on the board. Omit it and the first
|
|
32
|
+
clause of the prompt is used, which is usually the right answer.
|
|
33
|
+
|
|
34
|
+
It goes in the sprint in play on the board, as a card the user writes does. Pass `backlog=True` when they said it is for later — "another sprint", "the backlog" — and it stays out of every sprint.
|
|
35
|
+
|
|
36
|
+
Returns `{ id, title, lane, authored_by, sprint, room }`, where `sprint` says where it went and `room` is how many more cards the board will take from agents before the cap below refuses. In a loop, stop at `room == 0` rather than waiting for the error; a call that raises added nothing, so every call that returned is a card that landed.
|
|
37
|
+
|
|
38
|
+
## This is not api.todo_write
|
|
39
|
+
|
|
40
|
+
The two are easy to confuse and are not related.
|
|
41
|
+
|
|
42
|
+
| | `api.todo_write` | `api.add_card` |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| whose list | yours | the user's |
|
|
45
|
+
| what it is about | what you are doing now | what somebody should do |
|
|
46
|
+
| lifetime | replaced whole every call, gone on reload | durable, per workspace |
|
|
47
|
+
| where | the chat | the Work board |
|
|
48
|
+
|
|
49
|
+
`todo_write` is progress reporting. This is leaving work behind. Writing a card
|
|
50
|
+
never removes an earlier one — see [`todo_write`](todo_write.md) if what you
|
|
51
|
+
want is to show how far along you are.
|
|
52
|
+
|
|
53
|
+
## Judgement
|
|
54
|
+
|
|
55
|
+
Not gated, so nothing stops you. That makes the restraint yours:
|
|
56
|
+
|
|
57
|
+
- **No card the user did not ask for.** A general go-ahead from earlier in the
|
|
58
|
+
conversation does not cover something new, and neither does your own sense
|
|
59
|
+
that they would want it. A card is a claim on somebody's attention.
|
|
60
|
+
- **Work you were actually asked to do goes in your diff**, not on a card. A
|
|
61
|
+
card that says "finish the thing I was told to finish" is a way of not
|
|
62
|
+
finishing it.
|
|
63
|
+
- **A card is not a bug report.** If WidgeX itself got in your way, that is
|
|
64
|
+
[`report_bug`](report_bug.md) — it goes somewhere the WidgeX team reads,
|
|
65
|
+
where a card goes to whoever is using it.
|
|
66
|
+
- **A card is not a question.** If you need an answer to carry on, ask —
|
|
67
|
+
[`ask_user`](ask_user.md) blocks and a card does not.
|
|
68
|
+
|
|
69
|
+
There is a cap: twenty agent-written cards the user has not looked at yet. Hit
|
|
70
|
+
it and the call fails saying so. Acknowledged cards do not count, so a user who
|
|
71
|
+
is keeping up never meets it. The cap has no override and counts a card the user
|
|
72
|
+
asked for like any other: past it, put the work in your reply and offer to add the
|
|
73
|
+
cards once they have been through the board.
|
|
74
|
+
|
|
75
|
+
## Where it lands
|
|
76
|
+
|
|
77
|
+
The board is workspace-scoped, so the card goes to the board of the window your
|
|
78
|
+
session is running in. For a session the board itself started, that is the board
|
|
79
|
+
it came from. A session in a separately-opened worktree window writes to that
|
|
80
|
+
window's board.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
await api.add_card(
|
|
84
|
+
"Rename the `daemon` field in .widgex/bulletin.json to `harness`. "
|
|
85
|
+
"The bulletin is the last place still using the old word; the surface "
|
|
86
|
+
"enum is a Postgres CHECK constraint and needs a backend deploy first.",
|
|
87
|
+
title="Finish the daemon→harness rename",
|
|
88
|
+
)
|
|
89
|
+
```
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# agent vs fork
|
|
2
|
+
|
|
3
|
+
<!-- group: Concepts -->
|
|
4
|
+
|
|
5
|
+
Two ways to run work elsewhere. There used to be four, and three of them turned
|
|
6
|
+
out to be one call with different arguments.
|
|
7
|
+
|
|
8
|
+
## What collapsed
|
|
9
|
+
|
|
10
|
+
`delegate`, `session_extend` and `session_branch` are gone. They differed on two
|
|
11
|
+
questions, and those are now `api.agent`'s arguments:
|
|
12
|
+
|
|
13
|
+
| | `isolate=False` | `isolate=True` |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| **`wait=True`** | was `delegate` | was `session_branch` + `session_join` |
|
|
16
|
+
| **`wait=False`** | was `session_extend` | was `session_branch` |
|
|
17
|
+
|
|
18
|
+
The one thing `session_extend` had that a bare `wait=False` does not is the
|
|
19
|
+
guarantee that the child opens with a compacted summary of your session rather
|
|
20
|
+
than a pointer to it. That survived as `extend=True` — so the exact replacement
|
|
21
|
+
for the old call is `agent(prompt, extend=True, wait=False)`.
|
|
22
|
+
|
|
23
|
+
If you knew the old names, you already know this call. If you did not, you never
|
|
24
|
+
have to learn which of three bits of bookkeeping you wanted.
|
|
25
|
+
|
|
26
|
+
## What did not
|
|
27
|
+
|
|
28
|
+
`session_fork` stays separate, because it is a different intent rather than a
|
|
29
|
+
different setting:
|
|
30
|
+
|
|
31
|
+
| | `api.agent` | `session_fork` |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| Repo | your directory, or a worktree of the same repo | **a whole clone** |
|
|
34
|
+
| Context | fresh, from `prompt` + `summary` | **a copy of this conversation** |
|
|
35
|
+
| Work comes back | as text, or as a merge the user approves | **never** |
|
|
36
|
+
| Costs | a context; a worktree if isolated | real disk, real time, and your whole transcript |
|
|
37
|
+
|
|
38
|
+
Fork is for work that must not come back — the approach you rejected, tried
|
|
39
|
+
somewhere it cannot hurt anything. Everything you might actually want returned is
|
|
40
|
+
`agent`.
|
|
41
|
+
|
|
42
|
+
## Choosing
|
|
43
|
+
|
|
44
|
+
**You want an answer.** `api.agent(prompt, model=...)`. Blocks, hands back
|
|
45
|
+
`{text, model, turns, tokens}`, no session, no node, no permission prompt. This
|
|
46
|
+
is the default and it is underused.
|
|
47
|
+
|
|
48
|
+
**You want work done while you work, in your tree.**
|
|
49
|
+
`api.agent(..., wait=False)`. Returns immediately; the child edits the same files
|
|
50
|
+
you are editing.
|
|
51
|
+
|
|
52
|
+
**You want work done alongside yours, then merged.**
|
|
53
|
+
`api.agent(..., wait=False, isolate=True)`. Its own worktree and branch, so
|
|
54
|
+
neither of you disturbs the other, and the user can merge it afterwards. This is
|
|
55
|
+
the one for running several at once.
|
|
56
|
+
|
|
57
|
+
**The work must never come back.** `api.session_fork`.
|
|
58
|
+
|
|
59
|
+
## The costs you are trading
|
|
60
|
+
|
|
61
|
+
- **Waiting costs your turn.** An hour of work in a waited agent is an hour with
|
|
62
|
+
nothing else happening.
|
|
63
|
+
- **Not waiting costs a whole new context**, and — shared — a second agent
|
|
64
|
+
editing your files concurrently.
|
|
65
|
+
- **Isolating costs a working copy.** A worktree is cheap, it shares the object
|
|
66
|
+
store, but your uncommitted work is copied in and committed there as a seed, so
|
|
67
|
+
those edits then exist twice.
|
|
68
|
+
- **Forking costs a clone and your transcript.** Real disk, real time, and a
|
|
69
|
+
briefing method that also hands over everything irrelevant.
|
|
70
|
+
|
|
71
|
+
## Claude's own subagents
|
|
72
|
+
|
|
73
|
+
Untouched, and not this. The SDK's `Agent`/`Task` tool chooses between three
|
|
74
|
+
Claude tiers, runs inside the model's own machinery, and is invisible to the
|
|
75
|
+
user. `api.agent` reaches every model the user has configured and shows up in
|
|
76
|
+
their session list while it runs.
|
|
77
|
+
|
|
78
|
+
The rule is ownership: if you started it with `api.`, they can see it.
|
|
79
|
+
|
|
80
|
+
## Getting it wrong
|
|
81
|
+
|
|
82
|
+
**Waiting, then using only the text, when you could have delegated.** That is
|
|
83
|
+
what `wait=True` already is. It is the cheap path, not a lesser one.
|
|
84
|
+
|
|
85
|
+
**Not waiting, then joining immediately.** That is `wait=True` with a session, a
|
|
86
|
+
node and a permission prompt attached.
|
|
87
|
+
|
|
88
|
+
**Two shared agents at once.** Both edit your directory, at the same time as you
|
|
89
|
+
and each other. `isolate=True` is what keeps them apart.
|
|
90
|
+
|
|
91
|
+
**Two isolated agents on the same files.** Both merge cleanly and the second one
|
|
92
|
+
silently wins every shared file — git sees no conflict, because it treats the
|
|
93
|
+
first one's work as ordinary history. The wizard warns; split by file where you
|
|
94
|
+
can.
|
|
95
|
+
|
|
96
|
+
**Expecting an isolated agent to land itself.** The user merges it. "Ready to
|
|
97
|
+
merge" is true; "I've merged it" is not.
|
|
98
|
+
|
|
99
|
+
**Forking for parallelism.** Two clones of a large tree cost real disk and real
|
|
100
|
+
time. `isolate=True` shares the object store.
|
|
101
|
+
|
|
102
|
+
**Forgetting `model` on a waited, shared agent.** It is required there and
|
|
103
|
+
nowhere else: picking it is the point, and inheriting yours would put every
|
|
104
|
+
throwaway search on your most expensive model.
|
|
105
|
+
|
|
106
|
+
## See also
|
|
107
|
+
|
|
108
|
+
`agent`, `children`, `session_fork`, `session_join`, `sessions`, `xylon`
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# api.agent(prompt, model=None, isolate=False, wait=True, summary=None, aux_context=None, timeout_seconds=None, extend=False, machine=None, token_budget=None, must=None, raise_=False, title=None, task=None, harness=None) -> dict
|
|
2
|
+
|
|
3
|
+
<!-- group: Sessions -->
|
|
4
|
+
|
|
5
|
+
Hand work to another agent. Gated only when it outlives the call.
|
|
6
|
+
|
|
7
|
+
This replaced `delegate`, `session_extend` and `session_branch`, which differed
|
|
8
|
+
on a few questions and nothing else a caller should have had to think about.
|
|
9
|
+
Those questions are now arguments.
|
|
10
|
+
|
|
11
|
+
## The two questions
|
|
12
|
+
|
|
13
|
+
**Do you wait?** `wait=True` (default) runs it inside your turn and hands back
|
|
14
|
+
what it found. `wait=False` starts it and returns at once — collect it later with
|
|
15
|
+
`session_join`, or never.
|
|
16
|
+
|
|
17
|
+
**Does it get its own files?** `isolate=False` (default) means it works in your
|
|
18
|
+
directory, on your files, at the same time as you. `isolate=True` gives it a
|
|
19
|
+
worktree and a branch of its own, cut from what you can see right now.
|
|
20
|
+
|
|
21
|
+
| | `isolate=False` | `isolate=True` |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| **`wait=True`** | the cheap one — your shell, no session, no node, no prompt | start it, block on it, merge later |
|
|
24
|
+
| **`wait=False`** | a second agent in your tree, working while you work | the one for running several at once |
|
|
25
|
+
|
|
26
|
+
## And one guarantee: `extend`
|
|
27
|
+
|
|
28
|
+
By default a child starts cold. It gets your `prompt` and nothing else, and if
|
|
29
|
+
it has a node it is told it *may* read your transcript with
|
|
30
|
+
`xylon_read_messages('..')` — a fallback, not a plan, because transcripts are
|
|
31
|
+
large.
|
|
32
|
+
|
|
33
|
+
`extend=True` replaces that with a guarantee: **the child works in your working
|
|
34
|
+
tree, and it opens with a compacted summary of this session.** It is the old
|
|
35
|
+
`session_extend`, and it never hands over a bare pointer.
|
|
36
|
+
|
|
37
|
+
`extend=True` with `isolate=True` is refused — they contradict. Extend keeps
|
|
38
|
+
your working tree; isolate is the one that takes it away.
|
|
39
|
+
|
|
40
|
+
## The third thing: `raise_`
|
|
41
|
+
|
|
42
|
+
The two questions above both describe something you *spawn* — it hangs under
|
|
43
|
+
you, and it is yours to read back. `raise_=True` does not spawn anything. It
|
|
44
|
+
opens an **Issue**, through the same mechanism the `+` button drives: an Issue
|
|
45
|
+
record, a titled branch (`widgex/<title>`), and a working copy bound to it, with
|
|
46
|
+
a first session standing in it.
|
|
47
|
+
|
|
48
|
+
The difference from `isolate=True` is not mechanical — both end up on a branch
|
|
49
|
+
in a worktree — it is about whose work it is. A branch session is a piece of
|
|
50
|
+
*your* work you handed out and will merge back. An Issue is a separate piece of
|
|
51
|
+
work that happens to have started here: it stands beside yours in the Issue
|
|
52
|
+
list, the user lands and retires it with the gestures they already have, and it
|
|
53
|
+
outlives this call, this session and this window.
|
|
54
|
+
|
|
55
|
+
So: `isolate=True` when you are going to read it back yourself. `raise_=True`
|
|
56
|
+
when you are handing the user a thing, not an answer.
|
|
57
|
+
|
|
58
|
+
It always asks permission, including with `wait=True` — no combination of
|
|
59
|
+
arguments makes an Issue transient. It is refused with `isolate` or `extend`,
|
|
60
|
+
which each promise something it contradicts. It inherits your model unless you
|
|
61
|
+
name one, like any session.
|
|
62
|
+
|
|
63
|
+
Spelled with the trailing underscore because `raise` is a Python keyword, so
|
|
64
|
+
`api.agent(prompt, raise=True)` is a SyntaxError rather than an error message.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
child = await api.agent(
|
|
68
|
+
"Fix the two Windows-only failures in the extension suite. Details in card-3.",
|
|
69
|
+
raise_=True, wait=False,
|
|
70
|
+
title="Fix the Windows test failures",
|
|
71
|
+
summary="I'm mid-way through the document hub; this is unrelated and wants its own branch.",
|
|
72
|
+
)
|
|
73
|
+
return f"Raised {child['issueId']} on {child['branch']}, working in {child['repoDir']}."
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Check `stale_base`.** An Issue is cut from the commit the user's open folder
|
|
77
|
+
is sitting on, and nothing here fetches on a schedule — so if nobody has pulled
|
|
78
|
+
recently, the branch starts from work that is days old and the pull request is
|
|
79
|
+
where it surfaces. When the `+` button hits this it asks and offers to pull
|
|
80
|
+
first. You get no dialog, because moving somebody's checkout while they are
|
|
81
|
+
working in it is not an agent's call to make.
|
|
82
|
+
|
|
83
|
+
So the Issue is raised from what is there and `stale_base` carries the sentence:
|
|
84
|
+
`'main is 7 commits behind origin/main.'`, or `None` when there was nothing to
|
|
85
|
+
say. Pass it on — the user is the one who can decide whether to rebase it.
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
child = await api.agent("…", raise_=True, wait=False, title="Fix the flaky test")
|
|
89
|
+
if child['stale_base']:
|
|
90
|
+
return f"Raised {child['issueId']} on {child['branch']}. Note: {child['stale_base']}"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Options
|
|
94
|
+
|
|
95
|
+
- **`prompt`** — the whole task. It sees nothing of this conversation, so write
|
|
96
|
+
it as a brief for a stranger: the files, the goal, and the shape of the answer.
|
|
97
|
+
- **`model`** — e.g. `'claude-haiku-4-5'`, or `'provider/model'` when two
|
|
98
|
+
providers offer the same id. **Required when `wait=True` and
|
|
99
|
+
`isolate=False`**; inherited otherwise. An unknown id is refused with the list
|
|
100
|
+
of models available, and nothing is created.
|
|
101
|
+
- **`must`** — what the child has to be able to demonstrate, in plain words:
|
|
102
|
+
`must=["opens from the row", "survives a restart"]`.
|
|
103
|
+
|
|
104
|
+
**The order is the whole point.** An agent that writes its own claims after
|
|
105
|
+
implementing writes the ones it met — not dishonestly, it is describing what
|
|
106
|
+
it built, and what it built is what it can see. The result is a demo that
|
|
107
|
+
cannot fail, because the target was drawn around the arrow after it landed.
|
|
108
|
+
Dispatch is the only moment these are about the *task* rather than about the
|
|
109
|
+
implementation.
|
|
110
|
+
|
|
111
|
+
The child is told them, and reports which it met when it calls
|
|
112
|
+
`api.request_review`. It may add claims of its own; it cannot drop one of
|
|
113
|
+
these. An obligation named in neither `covers` nor `missing` is reported unmet
|
|
114
|
+
anyway, so there is nothing to gain by omitting one — and an unmet obligation
|
|
115
|
+
sorts the review row *up*, because the request that admits it needs attention
|
|
116
|
+
is the one most worth attention.
|
|
117
|
+
|
|
118
|
+
Needs a session to report against, so it is refused with `wait=True,
|
|
119
|
+
isolate=False`: that path answers inside your turn and you read the result
|
|
120
|
+
directly.
|
|
121
|
+
|
|
122
|
+
- **`raise_`** — open an Issue instead of spawning a child. See above. Always
|
|
123
|
+
asks; refused with `isolate` or `extend`.
|
|
124
|
+
- **`title`** — what the Issue is called, and therefore what its branch is
|
|
125
|
+
called. `raise_=True` only. Defaults to the first line of the prompt, which is
|
|
126
|
+
usually worth overriding: a branch name is read far more often than written.
|
|
127
|
+
- **`isolate`** — its own worktree and branch.
|
|
128
|
+
- **`wait`** — block and return its answer.
|
|
129
|
+
- **`extend`** — guarantee it your working tree and a compacted summary of this
|
|
130
|
+
session. Incompatible with `isolate`.
|
|
131
|
+
- **`summary`** — a paragraph telling a non-waited agent what you have been doing
|
|
132
|
+
and why it was spawned. Costs a fraction of a transcript, so prefer it whenever
|
|
133
|
+
the agent needs context. Ignored when `wait=True` *unless* `extend=True`, which
|
|
134
|
+
inlines it into the brief instead.
|
|
135
|
+
- **`aux_context`** — path to a `.md` or `.txt` file, read **now** and inlined
|
|
136
|
+
into its opening prompt. Capped at 100,000 characters.
|
|
137
|
+
- **`timeout_seconds`** — `wait=True` only. Defaults to 300.
|
|
138
|
+
- **`machine`** — a machine name from `.widgex/harnesses/`, or a path to a `.json`
|
|
139
|
+
file. Overlays a harness state machine on the child, which then drives it from
|
|
140
|
+
state to state instead of running one turn and stopping. Needs a session, so it
|
|
141
|
+
is refused on the cheap `wait=True, isolate=False` path. An unresolvable or
|
|
142
|
+
unparseable machine is refused with the parse errors, and **no child is
|
|
143
|
+
created**.
|
|
144
|
+
- **`token_budget`** — lifetime ceiling in tokens for a `machine` overlay.
|
|
145
|
+
Spending it stops the child until a person restarts it. Omit it for the
|
|
146
|
+
`widgex.harness.tokenCap` setting (400,000 by default). The child also stops
|
|
147
|
+
after 50 cycles, and its machine must be able to reach a terminal state.
|
|
148
|
+
Tokens are counted price-weighted (cache writes ×1.25, cache reads ×0.1), and
|
|
149
|
+
every turn carries the whole system prompt: **one short first turn has
|
|
150
|
+
measured about 60,000**, so a budget under ~100,000 can stop the child before
|
|
151
|
+
it has done anything. When that happens `session_join` says so —
|
|
152
|
+
`stopped_by: 'harness-budget'` — rather than reporting a crash.
|
|
153
|
+
- **`task`** — add this agent to an existing task (a worktree with its own
|
|
154
|
+
branch) instead of creating one. The id comes from `api.tasks()`. Cannot be
|
|
155
|
+
combined with `raise_`, `isolate` or `extend`. The Coordinator must use this
|
|
156
|
+
or `raise_` for any agent that outlives its turn. Always asks permission,
|
|
157
|
+
including with `wait=True`: it creates a session that outlives the call
|
|
158
|
+
regardless of `wait`, which only decides whether you block on its first turn.
|
|
159
|
+
- **`harness`** — which harness drives the agent — the same thing `machine`
|
|
160
|
+
names, under the name the rest of WidgeX uses. Give one or the other, not
|
|
161
|
+
both.
|
|
162
|
+
|
|
163
|
+
A non-extended agent given no `summary` is told it may read your transcript with
|
|
164
|
+
`xylon_read_messages('..')` if it needs the background. That is a fallback, not a
|
|
165
|
+
plan: transcripts are large.
|
|
166
|
+
|
|
167
|
+
## Where an extended child hangs, and what it opens with
|
|
168
|
+
|
|
169
|
+
One rule decides both:
|
|
170
|
+
|
|
171
|
+
> **An extended child hangs off the node whose summary it was given.**
|
|
172
|
+
|
|
173
|
+
**With `summary`.** It is banked on your node as a real checkpoint, you carry on
|
|
174
|
+
in a fresh `A<n>`, and the child hangs off the node that was sealed. A supplied
|
|
175
|
+
summary is treated as being as good as a checkpoint, so nothing is re-derived and
|
|
176
|
+
no compaction happens. Because it becomes your node's checkpoint, write it about
|
|
177
|
+
*the work* — the child's task belongs in `prompt`.
|
|
178
|
+
|
|
179
|
+
**Without `summary`.** Nothing is written to the tree. The child inherits the
|
|
180
|
+
nearest checkpoint above you — after a compaction, the node that was sealed then
|
|
181
|
+
— and hangs off *that* node, which makes it your sibling rather than your
|
|
182
|
+
descendant. If much has happened since, it also gets a budgeted account of it. If
|
|
183
|
+
you have never compacted there is no checkpoint to inherit, so the child hangs
|
|
184
|
+
off you and is briefed from the transcript instead.
|
|
185
|
+
|
|
186
|
+
`checkpoint` in the return names the node it opened from, either way.
|
|
187
|
+
|
|
188
|
+
Nothing asks you for a fresh summary here, and nothing can: this call runs inside
|
|
189
|
+
a tool call, and every summary-producing path in the IDE waits on a result that
|
|
190
|
+
your own tool call is blocking. That is why the guarantee is built from a
|
|
191
|
+
compaction baseline and the turns since it, both of which are already in memory.
|
|
192
|
+
|
|
193
|
+
## Returns
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
# wait=True, no session (the cheap path)
|
|
197
|
+
{"text": str, "model": str, "turns": int, "tokens": {...}}
|
|
198
|
+
# wait=True, with a session — the joined answer, plus what it is standing in
|
|
199
|
+
{..., "branch": str | None, "baseSha": str | None,
|
|
200
|
+
"issueId": str | None, "repoDir": str | None}
|
|
201
|
+
# wait=False
|
|
202
|
+
{"sessionId": str, "node": str, "branch": str | None, "baseSha": str | None,
|
|
203
|
+
"issueId": str | None, "repoDir": str | None,
|
|
204
|
+
"model": str | None, "machine": str | None, "machineState": str | None,
|
|
205
|
+
"checkpoint": str | None}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`issueId` and `repoDir` are filled only by `raise_=True`, and they are the two
|
|
209
|
+
facts you cannot recover afterwards from a session id alone: which Issue to
|
|
210
|
+
land, and which directory it is being done in.
|
|
211
|
+
|
|
212
|
+
## Why `model` is required for the cheap one
|
|
213
|
+
|
|
214
|
+
The whole point of running a search on a small model is that you chose to. A call
|
|
215
|
+
that silently inherited yours would put every throwaway grep on the most
|
|
216
|
+
expensive thing you have, which is the default that made model guidelines
|
|
217
|
+
necessary. Where the agent is a session it inherits, because a session is a
|
|
218
|
+
long-lived thing you can see and change the model of afterwards.
|
|
219
|
+
|
|
220
|
+
## Everything you start is visible
|
|
221
|
+
|
|
222
|
+
A waited agent appears under you in the session list while it runs, and
|
|
223
|
+
disappears when it returns — it has no node and no transcript, so there is
|
|
224
|
+
nothing to come back to. A session child stays.
|
|
225
|
+
|
|
226
|
+
This is the difference from Claude's own `Agent`/`Task` tool, which is not ours
|
|
227
|
+
and is not shown. If you started it with `api.`, the user can see it.
|
|
228
|
+
|
|
229
|
+
`api.children()` is the same information, for you.
|
|
230
|
+
|
|
231
|
+
## Isolated work does not land itself
|
|
232
|
+
|
|
233
|
+
An isolated agent's changes reach your branch only when the **user** merges
|
|
234
|
+
them. You cannot land them and neither can it. Report accurately: "it's ready to
|
|
235
|
+
merge" is true; "I've merged it" is not.
|
|
236
|
+
|
|
237
|
+
Your uncommitted work is copied into its worktree and committed there as a seed
|
|
238
|
+
commit — `WidgeX: parent's work in progress at the branch point` — so it starts
|
|
239
|
+
from the tree you are looking at. Your copy stays here, uncommitted.
|
|
240
|
+
|
|
241
|
+
Two consequences: those edits now exist twice, and drift if you keep editing
|
|
242
|
+
yours; and the seed commit travels back when its work lands, so the merge
|
|
243
|
+
reconciles two commits with the same content.
|
|
244
|
+
|
|
245
|
+
## Examples
|
|
246
|
+
|
|
247
|
+
A search you do not want in your context:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
r = await api.agent(
|
|
251
|
+
"Find every call site of retryBudget in src/ and report file:line.",
|
|
252
|
+
model="claude-haiku-4-5",
|
|
253
|
+
)
|
|
254
|
+
return r["text"]
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Two jobs at once, each landing separately:
|
|
258
|
+
|
|
259
|
+
```python
|
|
260
|
+
a = await api.agent("Add the fetch-on-land path in GitUtils, with tests.",
|
|
261
|
+
isolate=True, wait=False,
|
|
262
|
+
summary="We're splitting the landing work; you own the git layer.")
|
|
263
|
+
b = await api.agent("Add the queue row in surfaces.ts, with tests.",
|
|
264
|
+
isolate=True, wait=False,
|
|
265
|
+
summary="We're splitting the landing work; you own the panel layer.")
|
|
266
|
+
return f"Two branches going: {a['branch']} and {b['branch']}."
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
A long job in your own tree, checked on later:
|
|
270
|
+
|
|
271
|
+
```python
|
|
272
|
+
child = await api.agent(
|
|
273
|
+
"Port test/auth/*.test.mjs to the new fixture API. Report what broke.",
|
|
274
|
+
wait=False,
|
|
275
|
+
summary="I'm mid-refactor of src/auth; the fixture API changed shape today.",
|
|
276
|
+
)
|
|
277
|
+
return f"Started {child['sessionId']} on the auth tests."
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Continuing your own work in a second session, which is what `extend` is for:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
child = await api.agent(
|
|
284
|
+
"Finish porting the remaining fixtures in test/auth/, then run the suite.",
|
|
285
|
+
extend=True, wait=False,
|
|
286
|
+
)
|
|
287
|
+
return f"Handed off to {child['sessionId']}, opened from {child['checkpoint']}."
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
A child that drives itself through a state machine instead of stopping:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
return await api.agent(
|
|
294
|
+
"Keep the release notes current as commits land on main.",
|
|
295
|
+
wait=False, machine="release-notes", token_budget=200_000,
|
|
296
|
+
)
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Getting it wrong
|
|
300
|
+
|
|
301
|
+
**Waiting when you did not need to.** `wait=True` holds your turn. An hour of
|
|
302
|
+
work in a waited agent is an hour with nothing else happening.
|
|
303
|
+
|
|
304
|
+
**Not waiting when you only wanted an answer.** If you are going to
|
|
305
|
+
`session_join` immediately and use nothing but `lastMessage`, `wait=True` does
|
|
306
|
+
the same job without a session, a node or a permission prompt.
|
|
307
|
+
|
|
308
|
+
**Two shared-tree agents at once.** Both edit your directory, at the same time as
|
|
309
|
+
you and as each other. `isolate=True` is the one that keeps them apart.
|
|
310
|
+
|
|
311
|
+
**Two isolated agents on the same files.** Allowed, and both merge cleanly — git
|
|
312
|
+
sees no conflict, because whichever lands second treats the first one's work as
|
|
313
|
+
ordinary history and simply wins every file they share. The merge wizard warns
|
|
314
|
+
about the overlap. Split by file where you can.
|
|
315
|
+
|
|
316
|
+
**Expecting a subagent to fan out.** It cannot call `agent`, `fork`, `join` or
|
|
317
|
+
`terminate`. Work that needs sub-work needs a session — spawn it yourself.
|
|
318
|
+
|
|
319
|
+
**Speculative work that must never come back.** Isolated work *can* come back;
|
|
320
|
+
that is the point. `session_fork` is the one that cannot.
|
|
321
|
+
|
|
322
|
+
**Writing `extend`'s `summary` as a brief for the child.** It is stored as your
|
|
323
|
+
node's checkpoint and ranked against every other checkpoint in the tree from then
|
|
324
|
+
on, so "port the auth tests" describes the wrong session forever. Say what *you*
|
|
325
|
+
did; put the instruction in `prompt`.
|
|
326
|
+
|
|
327
|
+
**Expecting `..` to be your live node after an unbriefed `extend`.** The child
|
|
328
|
+
hangs off a checkpoint above you, so `..` is that checkpoint, not where you are
|
|
329
|
+
now. `checkpoint` in the return says which.
|
|
330
|
+
|
|
331
|
+
## See also
|
|
332
|
+
|
|
333
|
+
`children`, `session_join`, `session_fork`, `sessions`, `xylon`
|