@widgex/wgx 0.0.0-stage → 0.4.1
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 +4802 -0
- package/package.json +29 -4
- 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 +273753 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# api.session_rest(reason=None) -> dict
|
|
2
|
+
<!-- group: Sessions -->
|
|
3
|
+
|
|
4
|
+
Stop a harness re-serving its prompt. Not gated.
|
|
5
|
+
|
|
6
|
+
A harnessized session is handed the same prompt again at the start of every turn.
|
|
7
|
+
This is how that stops. The session stays alive and the harness binding stays
|
|
8
|
+
configured — a human can re-arm it from the Harness Manager without setting it up
|
|
9
|
+
again.
|
|
10
|
+
|
|
11
|
+
## Options
|
|
12
|
+
|
|
13
|
+
- **`reason`** — a short line saying why you stopped. It is shown to the user in
|
|
14
|
+
the Harness Manager, and it is the only explanation they get. `"done"` tells
|
|
15
|
+
them nothing; `"the migration is merged, so there is nothing left to poll
|
|
16
|
+
for"` tells them whether to re-arm.
|
|
17
|
+
|
|
18
|
+
## Returns
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
{"rested": True, "reason": str | None}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**`{"rested": False}`** when the session is not a harness. It does not raise —
|
|
25
|
+
calling it from a normal session is a no-op that reports itself as one.
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
|
|
29
|
+
Ending the loop when the work is actually finished:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
if not remaining:
|
|
33
|
+
await api.session_rest("all flaky tests are quarantined; nothing left to triage")
|
|
34
|
+
return "Done — I've stopped the harness."
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Stopping on a condition you cannot fix unattended:
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
if migration_needs_a_decision:
|
|
41
|
+
await api.session_rest(
|
|
42
|
+
"the schema change needs a call on whether to drop the legacy column; "
|
|
43
|
+
"I can't ask while unattended"
|
|
44
|
+
)
|
|
45
|
+
return "Rested — this needs a human decision before it can continue."
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Getting it wrong
|
|
49
|
+
|
|
50
|
+
**Using `session_terminate` instead.** Terminating destroys the session: the
|
|
51
|
+
transcript stops being a live thing you can re-arm, and the user has to build
|
|
52
|
+
the harness again from scratch. Resting is the reversible version, and it is
|
|
53
|
+
almost always the one you want.
|
|
54
|
+
|
|
55
|
+
**Never calling it.** A harness with no exit condition runs until it hits its
|
|
56
|
+
token or cycle limit and stops there, unfinished, waiting for a person. If your
|
|
57
|
+
prompt has a notion of "finished" that its machine cannot see, you are the only
|
|
58
|
+
thing that can notice.
|
|
59
|
+
|
|
60
|
+
**Resting because one turn had nothing to do.** A poll that finds no work is a
|
|
61
|
+
normal turn, not a reason to stop. Rest when the *task* is over, not when this
|
|
62
|
+
pass was quiet.
|
|
63
|
+
|
|
64
|
+
**Expecting it to end the turn.** It does not. Code after this call still runs
|
|
65
|
+
and your reply is still delivered. It changes what happens *next* turn.
|
|
66
|
+
|
|
67
|
+
**Calling it from a normal session and not checking the result.** You get
|
|
68
|
+
`{"rested": False}` and no error. If you thought you were a harness, you were
|
|
69
|
+
wrong, and nothing told you but the return value.
|
|
70
|
+
|
|
71
|
+
## See also
|
|
72
|
+
|
|
73
|
+
`session_harness`, `harness`, `session_terminate`, `sessions`
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# api.session_scope(scope) -> dict
|
|
2
|
+
<!-- also: scope, rename, session_name, what-am-i-working-on -->
|
|
3
|
+
<!-- group: Talking to the user -->
|
|
4
|
+
|
|
5
|
+
Say what part of the project this session is working on. Not gated.
|
|
6
|
+
|
|
7
|
+
One short phrase, in the words somebody **managing** the work would use — "the
|
|
8
|
+
sign-in flow", "release notarisation", "the kanban board". It becomes the top
|
|
9
|
+
line of this session's card on the board, its row in the sidebar, its editor tab,
|
|
10
|
+
and any queue row it raises.
|
|
11
|
+
|
|
12
|
+
## You usually do not need to call this
|
|
13
|
+
|
|
14
|
+
A session is already named from whatever it was first asked for, and that is
|
|
15
|
+
almost always right — a person wrote it, so it is in their language by
|
|
16
|
+
construction.
|
|
17
|
+
|
|
18
|
+
Call this when the work turned out to be about something else. A request to
|
|
19
|
+
"have a look at the build" that became notarisation is still shown as *"Have a
|
|
20
|
+
look at the build"* until you say otherwise, and somebody scanning six agents
|
|
21
|
+
will pick the wrong one. That is the whole use: one call, when the name has
|
|
22
|
+
stopped being true.
|
|
23
|
+
|
|
24
|
+
There is nothing to call at the start of a task, and nothing to repeat each turn.
|
|
25
|
+
|
|
26
|
+
## What this is not
|
|
27
|
+
|
|
28
|
+
**Not what you are doing right now.** That comes from the task list you already
|
|
29
|
+
keep with `todo_write` — the `activeForm` of whatever is `in_progress`. The two
|
|
30
|
+
are different questions with different lifetimes, which is why they are two lines
|
|
31
|
+
and not one string, and why only one of them needs a call.
|
|
32
|
+
|
|
33
|
+
**Not a file, a directory, a branch or a symbol.** `panel/board` and
|
|
34
|
+
`ClaudeSessionService.ts` are accurate and useless here: whoever reads this is
|
|
35
|
+
deciding which agent to look at next, not editing it. A branch is worse than
|
|
36
|
+
useless — every session in a worktree shares one, so it names the checkout and
|
|
37
|
+
tells you nothing about the agent.
|
|
38
|
+
|
|
39
|
+
**Not a rename over the user's head.** If they have renamed this session, theirs
|
|
40
|
+
is shown. Yours is kept and appears if they clear it.
|
|
41
|
+
|
|
42
|
+
## Options
|
|
43
|
+
|
|
44
|
+
- **`scope`** — the phrase. Long ones are cut at the first clause and then at 44
|
|
45
|
+
characters, so lead with the subject rather than the reason: "Release
|
|
46
|
+
notarisation" beats "Looking into why notarisation fails, which seems to be
|
|
47
|
+
about the keychain".
|
|
48
|
+
|
|
49
|
+
## Returns
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
{"scope": str | None} # what was recorded
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Examples
|
|
56
|
+
|
|
57
|
+
The case it exists for — the request and the work turned out to be different
|
|
58
|
+
things:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
# Started as "have a look at the build please".
|
|
62
|
+
await api.session_scope("Release notarisation")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Correcting a scope that has drifted, mid-session:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
await api.session_scope("Keychain access under launchd")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Getting it wrong
|
|
72
|
+
|
|
73
|
+
**Calling it every turn.** It is a standing claim, not a status. It persists
|
|
74
|
+
across a window reload precisely so it does not need restating.
|
|
75
|
+
|
|
76
|
+
**Naming the code.** "src/ide-widgets/panel" is the register this exists to
|
|
77
|
+
avoid. If the phrase contains a slash or a file extension, it is wrong.
|
|
78
|
+
|
|
79
|
+
**Using it to report progress.** "Finished the parser, now on tests" is two
|
|
80
|
+
statements and neither is a scope. Progress is `todo_write`.
|
|
81
|
+
|
|
82
|
+
## See also
|
|
83
|
+
|
|
84
|
+
`todo_write`, `request_review`, `agent`, `bulletin`
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# api.session_terminate(session_id) -> dict
|
|
2
|
+
<!-- group: Sessions -->
|
|
3
|
+
|
|
4
|
+
Stop this session or one of its descendants. Gated.
|
|
5
|
+
|
|
6
|
+
## Options
|
|
7
|
+
|
|
8
|
+
- **`session_id`** — the session to stop. Yourself, or something you spawned
|
|
9
|
+
(or something *it* spawned, to any depth).
|
|
10
|
+
|
|
11
|
+
Anything outside your own subtree is refused. You cannot terminate your parent,
|
|
12
|
+
a sibling, or an unrelated session.
|
|
13
|
+
|
|
14
|
+
## Returns
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
{"success": True}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Examples
|
|
21
|
+
|
|
22
|
+
Cleaning up a child that is no longer needed:
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
r = await api.session_join(child_id, timeout=600)
|
|
26
|
+
if r["status"] == "timeout":
|
|
27
|
+
await api.session_terminate(child_id)
|
|
28
|
+
return "That was taking too long; I stopped it."
|
|
29
|
+
return r["lastMessage"]
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Abandoning a fork after reading its answer:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
r = await api.session_join(fork_id, timeout=1800)
|
|
36
|
+
await api.session_terminate(fork_id)
|
|
37
|
+
return r.get("lastMessage")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Getting it wrong
|
|
41
|
+
|
|
42
|
+
**Terminating yourself to "finish".** Ending your turn is how you finish. This
|
|
43
|
+
call destroys the session, so anything the user says next has nowhere to go. If
|
|
44
|
+
you are a harness and want to stop looping, that is `api.session_rest`, which
|
|
45
|
+
leaves the session alive and re-armable.
|
|
46
|
+
|
|
47
|
+
**Terminating a child you have not joined.** Whatever it was going to report is
|
|
48
|
+
lost. Join first, read the result, then terminate.
|
|
49
|
+
|
|
50
|
+
**Expecting it to undo the child's work.** Files it wrote stay written.
|
|
51
|
+
Termination stops the conversation, not its effects. To see what it changed,
|
|
52
|
+
read its node with `xylon_get_diff`.
|
|
53
|
+
|
|
54
|
+
**Using it on a sibling.** Refused. The subtree rule has no override.
|
|
55
|
+
|
|
56
|
+
## See also
|
|
57
|
+
|
|
58
|
+
`session_rest`, `session_join`, `sessions`, `xylon_read`, `permissions`
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Session tokens: who is calling a tool
|
|
2
|
+
|
|
3
|
+
<!-- group: Concepts -->
|
|
4
|
+
<!-- also: WIDGEX_SESSION_TOKEN, harness identity, identity token, actor, segregation of duties, verify session -->
|
|
5
|
+
|
|
6
|
+
Every command a session runs through `api.run`, `api.bash` or `api.powershell`
|
|
7
|
+
has `WIDGEX_SESSION_TOKEN` in its environment. A tool you write — a ledger CLI,
|
|
8
|
+
a deploy script, a check — can hand that token back to WidgeX and be told which
|
|
9
|
+
session is calling, and which harness and state it is in **right now**.
|
|
10
|
+
|
|
11
|
+
It is optional at both ends. A tool that never looks at it is unaffected.
|
|
12
|
+
|
|
13
|
+
## Why it exists
|
|
14
|
+
|
|
15
|
+
A tool that enforces who may do what used to learn who was calling from flags
|
|
16
|
+
like `--actor` and `--role`, which the agent writes itself. That catches
|
|
17
|
+
mistakes, not an agent that claims to be someone else. A token is not
|
|
18
|
+
something an agent can type for another session: it is signed with a key that
|
|
19
|
+
exists only in the WidgeX window's memory, and each session only ever sees its
|
|
20
|
+
own.
|
|
21
|
+
|
|
22
|
+
## Checking one
|
|
23
|
+
|
|
24
|
+
Each WidgeX window writes where to ask into
|
|
25
|
+
`~/.widgex/harness-identity/<issuer>.json`. The issuer is the token's second
|
|
26
|
+
dot-separated field. Post the token to that file's `url`:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import json, os, pathlib, urllib.request
|
|
30
|
+
|
|
31
|
+
def who_is_calling():
|
|
32
|
+
token = os.environ.get("WIDGEX_SESSION_TOKEN")
|
|
33
|
+
if not token:
|
|
34
|
+
return None # not run from a WidgeX session
|
|
35
|
+
issuer = token.split(".")[1]
|
|
36
|
+
info = json.loads((pathlib.Path.home() / ".widgex" / "harness-identity" / f"{issuer}.json").read_text())
|
|
37
|
+
req = urllib.request.Request(info["url"], data=json.dumps({"token": token}).encode(),
|
|
38
|
+
headers={"content-type": "application/json"})
|
|
39
|
+
with urllib.request.urlopen(req, timeout=5) as r:
|
|
40
|
+
return json.load(r)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A valid token answers:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{"valid": true, "verified_at": 1792890000000,
|
|
47
|
+
"session_id": "95791dee-…", "node": "proj/repo/issue/session4", "label": "architect",
|
|
48
|
+
"harness": "revenue-desk", "harness_kind": "overlay", "state": "propose",
|
|
49
|
+
"parent_session_id": "b3588d9d-…"}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`harness`, `harness_kind` and `state` are `null` for a session not running one.
|
|
53
|
+
Anything else answers `{"valid": false, "reason": "..."}`. A tool enforcing a
|
|
54
|
+
rule should refuse on that, not fall back to a flag.
|
|
55
|
+
|
|
56
|
+
Read the url from the file, never from an environment variable: the agent
|
|
57
|
+
chooses its commands' environment, and a url it set could point anywhere.
|
|
58
|
+
|
|
59
|
+
## What it proves
|
|
60
|
+
|
|
61
|
+
- **Which session's commands the call came from.** Session A cannot pass
|
|
62
|
+
itself off as session B.
|
|
63
|
+
- **Its harness and state when you asked**, not when the command started.
|
|
64
|
+
|
|
65
|
+
What it does not prove:
|
|
66
|
+
|
|
67
|
+
- That the session *should* be doing this. Your tool's rules still decide that.
|
|
68
|
+
- Anything against code that sets out to subvert WidgeX on the same account,
|
|
69
|
+
for instance by rewriting the discovery file. The token makes lying mean
|
|
70
|
+
doing that, instead of typing a different `--actor`.
|
|
71
|
+
|
|
72
|
+
## Lifetime
|
|
73
|
+
|
|
74
|
+
A token is valid for as long as its session exists and the window that issued
|
|
75
|
+
it is open. After a window reload the old tokens stop verifying
|
|
76
|
+
(`"issued by another WidgeX window, or by this one before it reloaded"`), and
|
|
77
|
+
every shell started from then on carries a new one. Sessions running on a
|
|
78
|
+
remote host carry no token.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Sessions
|
|
2
|
+
<!-- group: Concepts -->
|
|
3
|
+
|
|
4
|
+
What a session is, how children relate to parents, and which call makes which.
|
|
5
|
+
|
|
6
|
+
A session is one conversation with one model, running in one working directory,
|
|
7
|
+
occupying one node in a xylon tree. You are a session. Everything you can spawn
|
|
8
|
+
is either a *child session* — its own conversation, its own node, running
|
|
9
|
+
independently — or a *subagent*, which is neither.
|
|
10
|
+
|
|
11
|
+
## The shapes
|
|
12
|
+
|
|
13
|
+
| Call | Repo | Conversation | Node | Runs |
|
|
14
|
+
|---|---|---|---|---|
|
|
15
|
+
| `agent` | **same** working dir | fresh, briefed by `summary` | child of yours | independently |
|
|
16
|
+
| `session_fork` | **cloned** copy | **copy of yours** | child of yours | independently |
|
|
17
|
+
| `agent` | same | fresh, briefed by `prompt` | none | **blocks you** |
|
|
18
|
+
| `session_harness` | this session | this one | this one | re-serves you |
|
|
19
|
+
|
|
20
|
+
Only `agent` blocks. `extend` and `fork` return as soon as the child exists;
|
|
21
|
+
you wait for one with `session_join` or you do not wait at all.
|
|
22
|
+
|
|
23
|
+
## Talking to a child
|
|
24
|
+
|
|
25
|
+
At birth a child gets exactly three things:
|
|
26
|
+
|
|
27
|
+
1. its `prompt`,
|
|
28
|
+
2. whatever `aux_context` file you attached, inlined into that prompt,
|
|
29
|
+
3. `summary`, if you passed one (`agent` only).
|
|
30
|
+
|
|
31
|
+
It can also read your side of the tree with `xylon_read_messages('..')` — that
|
|
32
|
+
is the fallback a child is told about when you give it no `summary` — and you
|
|
33
|
+
can read its result with `session_join`.
|
|
34
|
+
|
|
35
|
+
After that, there is a channel: see `relay`. `api.relay_connect(node)` asks any
|
|
36
|
+
session for one and it may decline; `force=True` takes one on a **direct child**
|
|
37
|
+
without asking, and turns your `relay_send` into that child's prompt, blocking
|
|
38
|
+
until it answers. That is how you steer a child that has misread the job,
|
|
39
|
+
instead of waiting for it to finish being wrong.
|
|
40
|
+
|
|
41
|
+
What every agent in this codebase is *currently* working on is a separate
|
|
42
|
+
thing, and is on the `bulletin`.
|
|
43
|
+
|
|
44
|
+
## Waiting, and not waiting
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
child = await api.agent(
|
|
48
|
+
"Port the auth tests to the new fixture API and report what broke.",
|
|
49
|
+
summary="I'm mid-refactor of src/auth; the fixture API changed shape today.",
|
|
50
|
+
)
|
|
51
|
+
r = await api.session_join(child["sessionId"], timeout=600)
|
|
52
|
+
if r["status"] != "done":
|
|
53
|
+
return {"child": child["sessionId"], "status": r["status"], "why": r.get("error")}
|
|
54
|
+
return r["lastMessage"]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Fire and forget is legitimate — a child that will take an hour should not hold
|
|
58
|
+
your turn open. Report the session id and move on.
|
|
59
|
+
|
|
60
|
+
## Terminating
|
|
61
|
+
|
|
62
|
+
`session_terminate` works on **yourself or a descendant only**. A parent, a
|
|
63
|
+
sibling, or an unrelated session is refused. There is no way to kill something
|
|
64
|
+
that did not come from you.
|
|
65
|
+
|
|
66
|
+
## Permission
|
|
67
|
+
|
|
68
|
+
`session_fork`, `agent`, `session_terminate` and `session_harness` all prompt
|
|
69
|
+
the user. `session_join` and `session_rest` never
|
|
70
|
+
do — waiting and stopping need no approval.
|
|
71
|
+
|
|
72
|
+
## Getting it wrong
|
|
73
|
+
|
|
74
|
+
**Spawning a child to do something you could just do.** A child costs a whole
|
|
75
|
+
new context, and the answer has to travel back through `session_join` as a
|
|
76
|
+
single string. For work you want the answer from, `agent` is cheaper and
|
|
77
|
+
returns directly.
|
|
78
|
+
|
|
79
|
+
**Forking when you meant to extend.** A fork clones the repository. Two
|
|
80
|
+
sessions then edit two different copies of the same files, and nothing merges
|
|
81
|
+
them. Fork when the work is speculative or must not touch the user's tree;
|
|
82
|
+
extend when it is the same tree at a different scope.
|
|
83
|
+
|
|
84
|
+
**Assuming a child inherits the conversation.** Only `session_fork` copies the
|
|
85
|
+
history. `agent` gives the child nothing but what you wrote.
|
|
86
|
+
|
|
87
|
+
**Reading `lastMessage` without checking `status`.** A failed child returns an
|
|
88
|
+
empty `lastMessage`, which reads exactly like a child that had nothing to say.
|
|
89
|
+
|
|
90
|
+
## See also
|
|
91
|
+
|
|
92
|
+
`agent-vs-fork`, `agent`, `session_fork`, `session_join`,
|
|
93
|
+
`session_terminate`, `agent`, `xylon`, `harness`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# api.set_harness(agent, harness, brief=None) -> dict
|
|
2
|
+
|
|
3
|
+
<!-- group: Sessions -->
|
|
4
|
+
|
|
5
|
+
Put an agent under a harness — a state machine that drives it state by state.
|
|
6
|
+
|
|
7
|
+
`agent` is an id from `api.tasks()`. `harness` is a harness name from
|
|
8
|
+
`.widgex/harnesses/`. `brief` is the standing instruction the harness re-serves
|
|
9
|
+
above every state; it defaults to what the agent was first asked.
|
|
10
|
+
|
|
11
|
+
If the agent is busy, the harness arms and starts when its current turn ends.
|
|
12
|
+
|
|
13
|
+
## Returns
|
|
14
|
+
|
|
15
|
+
`{"agent": str, "harness": str, "started": bool}`
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# api.spawn_swarm(shared_prompt, given, model=None, workflow=None, browser=False) -> dict
|
|
2
|
+
|
|
3
|
+
<!-- also: swarm, agent_swarm -->
|
|
4
|
+
<!-- group: Sessions -->
|
|
5
|
+
|
|
6
|
+
Start an agent swarm: one autonomous agent per prompt in `given`, all working on
|
|
7
|
+
the same kind of problem at once. **Always asks the user**, in every mode —
|
|
8
|
+
Autonomous included — because the card is the only moment a person sees how
|
|
9
|
+
many agents are about to start.
|
|
10
|
+
|
|
11
|
+
Each agent:
|
|
12
|
+
|
|
13
|
+
- is sent your `shared_prompt` first, then a divider, then its own `given` prompt;
|
|
14
|
+
- gets a worktree and a branch of its own, cut from **your task's branch** — the
|
|
15
|
+
worktrees are made in parallel, so twenty cost little more than one;
|
|
16
|
+
- runs Autonomous, on `model` (or yours), under `workflow` if you name a harness.
|
|
17
|
+
|
|
18
|
+
Together they are one **Agent Swarm** row under your task, with counters for
|
|
19
|
+
idle, blocked, running and finished. The user opens the row to read every
|
|
20
|
+
agent's briefs in one stream, message all of them at once, and answer their
|
|
21
|
+
permission requests and questions; and merges, halts, wakes or aborts them from
|
|
22
|
+
the row's menu. When the task is shelved or done, every agent's work is
|
|
23
|
+
committed and its worktree removed.
|
|
24
|
+
|
|
25
|
+
Returns at once once approved; nothing waits on the agents. The approval itself
|
|
26
|
+
waits on a person, so give the exec a generous `timeout_seconds` (600 or more).
|
|
27
|
+
Calling again with the same prompts while that swarm is live hands back the one
|
|
28
|
+
already running (`already: True`) rather than starting it twice.
|
|
29
|
+
|
|
30
|
+
## Arguments
|
|
31
|
+
|
|
32
|
+
- **`shared_prompt`** — a path to a file, relative to the cwd, or the prompt
|
|
33
|
+
text itself. A single word with an extension that names no file is refused,
|
|
34
|
+
rather than sent to twenty agents as their brief.
|
|
35
|
+
- **`given`** — a list, one entry per agent, each a path or text like
|
|
36
|
+
`shared_prompt`. At most 50.
|
|
37
|
+
- **`model`** — e.g. `'claude-sonnet-5-5'`. `'Claude-Sonnet-5.5'` is accepted as
|
|
38
|
+
the same id. An unknown one is refused with the list, and nothing is made.
|
|
39
|
+
- **`workflow`** — a harness name or a path to its `.json`. It must have a
|
|
40
|
+
terminal state, or the agents could never stop.
|
|
41
|
+
- **`browser`** — whether the agents may use the browser. `False` refuses
|
|
42
|
+
`drive`, `use_widget` and the `browser_*` calls to them.
|
|
43
|
+
|
|
44
|
+
## Returns
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
{"swarm": "swarm-…", "task": "issue-12", "branch": "widgex/…",
|
|
48
|
+
"members": [{"index": 1, "session": "session-40", "node": "…",
|
|
49
|
+
"branch": "widgex/…", "label": "#1 …"}],
|
|
50
|
+
"failed": [{"index": 3, "label": "#3 …", "error": "…"}]}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
A member whose worktree could not be made is in `failed`; the rest still start.
|
|
54
|
+
|
|
55
|
+
## What the agents are told
|
|
56
|
+
|
|
57
|
+
That they are one of N, that the others are on other branches, to report with
|
|
58
|
+
`api.swarm_brief(text)` as they go, and to commit and call `api.request_review`
|
|
59
|
+
when done — which marks them finished on the row instead of filing an inbox row.
|
|
60
|
+
|
|
61
|
+
## Examples
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
files = [f"tasks/{n}.md" for n in range(1, 9)]
|
|
65
|
+
r = await api.spawn_swarm("tasks/shared.md", files, model="claude-sonnet-5-5")
|
|
66
|
+
return f"{len(r['members'])} agents started, {len(r['failed'])} failed"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Prompts as text:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
modules = ["parser", "lexer", "printer"]
|
|
73
|
+
await api.spawn_swarm(
|
|
74
|
+
"Add property-based tests to one module. Keep the public API unchanged.",
|
|
75
|
+
[f"The module is src/{m}.ts." for m in modules],
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Run it from an agent in a task, not from the main checkout — the agents are cut
|
|
80
|
+
from the task's branch, and the Coordinator has none.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# api.submit_evidence(id, note, files=None) -> dict
|
|
2
|
+
<!-- group: Sessions -->
|
|
3
|
+
|
|
4
|
+
Attach one required artifact to the harness state you are in. Not gated.
|
|
5
|
+
|
|
6
|
+
Some harness states are **evidence gates**: they cannot be left until every
|
|
7
|
+
required artifact has been submitted, no matter what you answer in the
|
|
8
|
+
transition block. The prompt for such a state lists what is still owed, by id.
|
|
9
|
+
This is how you hand it over.
|
|
10
|
+
|
|
11
|
+
Answering the transition questions does not move a harness that still owes
|
|
12
|
+
evidence. That is deliberate — an agent that believes it has finished is exactly
|
|
13
|
+
the one whose word is worth least, and the gate exists so that belief is not the
|
|
14
|
+
thing being tested.
|
|
15
|
+
|
|
16
|
+
## Options
|
|
17
|
+
|
|
18
|
+
- **`id`** — the artifact id as written in the state's prompt: lowercase, and
|
|
19
|
+
one of the ids that state actually requires. Anything else raises, and the
|
|
20
|
+
message names the ids that were expected. You cannot invent one.
|
|
21
|
+
- **`note`** — the evidence itself. The command you ran and what it printed, not
|
|
22
|
+
a claim that you ran it. An empty note is refused, because a submission that
|
|
23
|
+
says nothing is indistinguishable from not having done the work.
|
|
24
|
+
- **`files`** — optional paths backing the claim.
|
|
25
|
+
|
|
26
|
+
Re-submitting the same id during the same visit to a state **replaces** your
|
|
27
|
+
earlier note rather than adding to it, so re-running a failing test and
|
|
28
|
+
submitting again is the normal way to correct the record.
|
|
29
|
+
|
|
30
|
+
## Returns
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
{"submitted": True, "still_missing": ["diff", "visual"], "state": "evidence"}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`still_missing` is what the gate is *now* waiting for. An empty list means the
|
|
37
|
+
gate is satisfied and the next transition you answer true can move.
|
|
38
|
+
|
|
39
|
+
## Raises
|
|
40
|
+
|
|
41
|
+
`ValueError` when the session is not running under a harness, when the state
|
|
42
|
+
collects no evidence, or when `id` is not one of that state's artifacts.
|
|
43
|
+
|
|
44
|
+
## Freshness
|
|
45
|
+
|
|
46
|
+
Submissions are scoped to the current **visit** to the state. If a review sends
|
|
47
|
+
work back and the run later re-enters the gate, last round's proof does not
|
|
48
|
+
count and every required artifact must be submitted again — which is the whole
|
|
49
|
+
point of sending it back.
|
|
50
|
+
|
|
51
|
+
## See also
|
|
52
|
+
|
|
53
|
+
- `request_review` — say a whole piece of work is ready for a person to look at.
|
|
54
|
+
- `session_rest` — stop, when another cycle would add nothing.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# api.swarm_brief(text) -> dict
|
|
2
|
+
|
|
3
|
+
<!-- group: Sessions -->
|
|
4
|
+
|
|
5
|
+
Report progress to your agent swarm's stream. Never asks. Only for an agent
|
|
6
|
+
started by `api.spawn_swarm`; anyone else is refused.
|
|
7
|
+
|
|
8
|
+
The user reads every agent's briefs side by side in one stream, so a brief is
|
|
9
|
+
one or two sentences about **what changed** — not what you are about to try,
|
|
10
|
+
and not a log. Write one when you have started, at each milestone, and before
|
|
11
|
+
you stop. A turn that ends without one has its last message posted in its
|
|
12
|
+
place, which is rarely as clear as one you wrote.
|
|
13
|
+
|
|
14
|
+
Finishing is not a brief: commit your work and call `api.request_review(claim)`.
|
|
15
|
+
That marks you finished on the swarm's row.
|
|
16
|
+
|
|
17
|
+
Messages the user sends to the whole swarm arrive in your tool results or as a
|
|
18
|
+
prompt. Answer them in your reply — the reply is shown in the stream.
|
|
19
|
+
|
|
20
|
+
## Returns
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
{"ok": True, "swarm": "swarm-…"}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Example
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
await api.swarm_brief("Parser tests in: 14 properties, 2 found real bugs (fixed, see commit).")
|
|
30
|
+
```
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# api.task_status(status, detail=None) -> dict
|
|
2
|
+
|
|
3
|
+
<!-- group: Sessions -->
|
|
4
|
+
|
|
5
|
+
Say where your task has got to. Only the task's lead may.
|
|
6
|
+
|
|
7
|
+
The one thing on the user's board that is **told** rather than read.
|
|
8
|
+
|
|
9
|
+
Everything else a card shows is derived from your session: which lane it is in
|
|
10
|
+
comes from your permission mode and whether you have asked anybody to look at
|
|
11
|
+
something, the badge comes from your status, the branch comes from your Issue.
|
|
12
|
+
That works for every question a session can be *observed* to answer.
|
|
13
|
+
|
|
14
|
+
"How is this going" is not one of them. A session three files into a refactor
|
|
15
|
+
and a session stuck on the second look identical from outside — same status,
|
|
16
|
+
same lane, same branch — and you are the only thing in the system that knows
|
|
17
|
+
which one you are.
|
|
18
|
+
|
|
19
|
+
## Who may call it
|
|
20
|
+
|
|
21
|
+
The task's **lead**: the first agent spun up in it, which is the one answerable
|
|
22
|
+
for the task and whose state the task already shows. `api.tasks()` reports
|
|
23
|
+
`lead` on every task, so you can check rather than assume.
|
|
24
|
+
|
|
25
|
+
An aux agent is refused, and the refusal names the lead. That is not a
|
|
26
|
+
formality — a task is one working copy with one agent answerable for it and any
|
|
27
|
+
number of agents helping, so a status any of them could write would be the group
|
|
28
|
+
talking over the one that answers for it. Report to your lead with
|
|
29
|
+
`api.relay_send` and let it speak for the task.
|
|
30
|
+
|
|
31
|
+
Outside a task it is refused too. The main checkout is the folder the user has
|
|
32
|
+
open; nobody is answerable for it, so a status there would describe the window
|
|
33
|
+
rather than any piece of work. Work that wants a status wants a task:
|
|
34
|
+
`api.agent(raise_=True, title=...)`.
|
|
35
|
+
|
|
36
|
+
## Options
|
|
37
|
+
|
|
38
|
+
- **`status`** — where the work is, in a few words. *"reading the importer"*,
|
|
39
|
+
*"blocked on the schema"*, *"landing it"*. One line, up to 60 characters,
|
|
40
|
+
because it is drawn on a card about 200px wide. Longer is refused rather than
|
|
41
|
+
silently cut, so you choose the words instead of a slice choosing them.
|
|
42
|
+
- **`detail`** — one sentence more, when the label cannot carry it. Shown when
|
|
43
|
+
somebody hovers the card. Up to 400 characters.
|
|
44
|
+
|
|
45
|
+
## Returns
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
{"task": str, "status": str, "detail": str | None, "at": int}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`task` is the id it was recorded against, and `status` is what was stored after
|
|
52
|
+
being collapsed to a single line.
|
|
53
|
+
|
|
54
|
+
## When to call it
|
|
55
|
+
|
|
56
|
+
When the answer changes. You have understood the problem; you have started
|
|
57
|
+
building; you are blocked on something; you are landing.
|
|
58
|
+
|
|
59
|
+
It is a **position, not a log**. A line that changes every thirty seconds is a
|
|
60
|
+
line nobody reads, and nothing here is a progress feed — the transcript already
|
|
61
|
+
holds what happened, and `api.todo_write` already holds your own checklist.
|
|
62
|
+
|
|
63
|
+
## Getting it wrong
|
|
64
|
+
|
|
65
|
+
**Using it to hand work back.** It is not `api.phase_complete`. This says
|
|
66
|
+
where the work is; that says the work is done and asks a human to judge it.
|
|
67
|
+
Writing "finished" here hands nothing back, files nothing on anybody's queue,
|
|
68
|
+
and nobody is asked to look.
|
|
69
|
+
|
|
70
|
+
**Writing it per tool call.** See above: a position, not a log.
|
|
71
|
+
|
|
72
|
+
**Calling it as an aux agent and treating the refusal as a bug.** It names your
|
|
73
|
+
lead because relaying to the lead is the thing to do instead.
|
|
74
|
+
|
|
75
|
+
**Expecting it to move a card between lanes.** Lanes are claims about the world
|
|
76
|
+
and are derived — see `update_card` for the one call that places a card, and
|
|
77
|
+
note that it is for an orchestrator pointing a card at a subagent, which is a
|
|
78
|
+
different job from this.
|
|
79
|
+
|
|
80
|
+
## See also
|
|
81
|
+
|
|
82
|
+
`tasks`, `update_card`, `phase_complete`, `relay`
|