@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.
Files changed (131) hide show
  1. package/LICENSE.txt +49 -0
  2. package/README.md +33 -2
  3. package/ThirdPartyNotices.txt +4685 -0
  4. package/package.json +30 -4
  5. package/provider-clis.json +1211 -0
  6. package/python/XACTOR.md +146 -0
  7. package/python/browser_actor_host.py +456 -0
  8. package/python/cdp_page.py +363 -0
  9. package/python/harness_check.py +77 -0
  10. package/python/requirements.txt +1 -0
  11. package/python/test_line_endings.py +185 -0
  12. package/python/xactor/__init__.py +18 -0
  13. package/python/xactor/__main__.py +226 -0
  14. package/python/xactor/api.py +882 -0
  15. package/python/xactor/bridge.py +784 -0
  16. package/python/xactor/dom.py +117 -0
  17. package/python/xactor/man/add_card.md +89 -0
  18. package/python/xactor/man/agent-vs-fork.md +108 -0
  19. package/python/xactor/man/agent.md +333 -0
  20. package/python/xactor/man/ask_model.md +110 -0
  21. package/python/xactor/man/ask_reference.md +56 -0
  22. package/python/xactor/man/ask_user.md +103 -0
  23. package/python/xactor/man/attach_app.md +595 -0
  24. package/python/xactor/man/attach_ide.md +65 -0
  25. package/python/xactor/man/attach_ide_surface.md +31 -0
  26. package/python/xactor/man/await_user.md +214 -0
  27. package/python/xactor/man/bash.md +142 -0
  28. package/python/xactor/man/browser-driver.md +137 -0
  29. package/python/xactor/man/browser_activity.md +53 -0
  30. package/python/xactor/man/browser_attach.md +84 -0
  31. package/python/xactor/man/browser_close_tab.md +50 -0
  32. package/python/xactor/man/browser_detach.md +50 -0
  33. package/python/xactor/man/browser_exec.md +66 -0
  34. package/python/xactor/man/browser_focus_tab.md +47 -0
  35. package/python/xactor/man/browser_hybrid.md +47 -0
  36. package/python/xactor/man/browser_open.md +50 -0
  37. package/python/xactor/man/browser_open_tab.md +58 -0
  38. package/python/xactor/man/browser_plumbing.md +107 -0
  39. package/python/xactor/man/browser_screenshot.md +56 -0
  40. package/python/xactor/man/browser_snapshot.md +52 -0
  41. package/python/xactor/man/browser_tabs.md +67 -0
  42. package/python/xactor/man/bulletin.md +88 -0
  43. package/python/xactor/man/capture_page.md +54 -0
  44. package/python/xactor/man/cd.md +64 -0
  45. package/python/xactor/man/chess_move.md +53 -0
  46. package/python/xactor/man/children.md +89 -0
  47. package/python/xactor/man/close_task.md +14 -0
  48. package/python/xactor/man/compact.md +75 -0
  49. package/python/xactor/man/drive.md +140 -0
  50. package/python/xactor/man/drive_space.md +72 -0
  51. package/python/xactor/man/driving.md +168 -0
  52. package/python/xactor/man/edit_file.md +112 -0
  53. package/python/xactor/man/finding.md +194 -0
  54. package/python/xactor/man/fork_agent.md +45 -0
  55. package/python/xactor/man/glob.md +83 -0
  56. package/python/xactor/man/grep.md +115 -0
  57. package/python/xactor/man/harness.md +165 -0
  58. package/python/xactor/man/harness_check.md +70 -0
  59. package/python/xactor/man/harness_format.md +257 -0
  60. package/python/xactor/man/highlight.md +66 -0
  61. package/python/xactor/man/instruct.md +111 -0
  62. package/python/xactor/man/ls.md +63 -0
  63. package/python/xactor/man/man.md +86 -0
  64. package/python/xactor/man/merge_wiz.md +40 -0
  65. package/python/xactor/man/notebook.md +98 -0
  66. package/python/xactor/man/overview.md +89 -0
  67. package/python/xactor/man/permissions.md +137 -0
  68. package/python/xactor/man/phase.md +150 -0
  69. package/python/xactor/man/powershell.md +74 -0
  70. package/python/xactor/man/profiles.md +60 -0
  71. package/python/xactor/man/project_drive.md +49 -0
  72. package/python/xactor/man/read_bulletin.md +89 -0
  73. package/python/xactor/man/read_file.md +90 -0
  74. package/python/xactor/man/relay.md +216 -0
  75. package/python/xactor/man/reply.md +72 -0
  76. package/python/xactor/man/reply_message.md +78 -0
  77. package/python/xactor/man/report_bug.md +118 -0
  78. package/python/xactor/man/request_answer.md +142 -0
  79. package/python/xactor/man/request_merge_decisions.md +121 -0
  80. package/python/xactor/man/request_operator_action.md +152 -0
  81. package/python/xactor/man/request_review.md +122 -0
  82. package/python/xactor/man/request_user_action.md +126 -0
  83. package/python/xactor/man/return_card.md +71 -0
  84. package/python/xactor/man/run.md +99 -0
  85. package/python/xactor/man/sandbox.md +144 -0
  86. package/python/xactor/man/session_fork.md +74 -0
  87. package/python/xactor/man/session_harness.md +109 -0
  88. package/python/xactor/man/session_join.md +102 -0
  89. package/python/xactor/man/session_land.md +73 -0
  90. package/python/xactor/man/session_nudge.md +73 -0
  91. package/python/xactor/man/session_rest.md +73 -0
  92. package/python/xactor/man/session_scope.md +84 -0
  93. package/python/xactor/man/session_terminate.md +58 -0
  94. package/python/xactor/man/session_token.md +78 -0
  95. package/python/xactor/man/sessions.md +93 -0
  96. package/python/xactor/man/set_harness.md +15 -0
  97. package/python/xactor/man/spawn_swarm.md +80 -0
  98. package/python/xactor/man/submit_evidence.md +54 -0
  99. package/python/xactor/man/swarm_brief.md +30 -0
  100. package/python/xactor/man/task_status.md +82 -0
  101. package/python/xactor/man/tasks.md +47 -0
  102. package/python/xactor/man/todo_write.md +104 -0
  103. package/python/xactor/man/update_card.md +70 -0
  104. package/python/xactor/man/use_profile.md +90 -0
  105. package/python/xactor/man/use_widget.md +82 -0
  106. package/python/xactor/man/wait_for_task.md +105 -0
  107. package/python/xactor/man/widget-model.md +284 -0
  108. package/python/xactor/man/widgets.md +59 -0
  109. package/python/xactor/man/write_file.md +76 -0
  110. package/python/xactor/man/writing-a-widget.md +142 -0
  111. package/python/xactor/man/xylon.md +120 -0
  112. package/python/xactor/man/xylon_blame.md +80 -0
  113. package/python/xactor/man/xylon_read.md +131 -0
  114. package/python/xactor/man/xylon_search.md +125 -0
  115. package/python/xactor/manual.py +195 -0
  116. package/python/xactor/parent_watch.py +115 -0
  117. package/python/xactor/plan_mode.py +350 -0
  118. package/python/xactor/sandbox.py +856 -0
  119. package/python/xactor/seals.py +438 -0
  120. package/python/xactor/shell_hints.py +210 -0
  121. package/python/xactor/states/__init__.py +13 -0
  122. package/python/xactor/states/base.py +523 -0
  123. package/python/xactor/states/registry.py +274 -0
  124. package/python/xactor/stdlib_api.py +6345 -0
  125. package/python/xactor/widgets/__init__.py +1 -0
  126. package/python/xactor/widgets/github_tokens.py +314 -0
  127. package/python/xactor/widgets/google_aistudio.py +365 -0
  128. package/settings-schema.json +403 -0
  129. package/viewer/index.css +160 -0
  130. package/viewer/index.js +15 -0
  131. 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`