aworg 1.0.0__py3-none-any.whl
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.
- aworg/__init__.py +33 -0
- aworg/__main__.py +6 -0
- aworg/activities.py +288 -0
- aworg/agent.py +621 -0
- aworg/auth.py +183 -0
- aworg/cli.py +372 -0
- aworg/host.py +382 -0
- aworg/inbox.py +112 -0
- aworg/install.py +502 -0
- aworg/journal.py +448 -0
- aworg/layout.py +199 -0
- aworg/models/__init__.py +102 -0
- aworg/models/anthropic.py +257 -0
- aworg/models/base.py +319 -0
- aworg/models/openai_compatible.py +649 -0
- aworg/models/openai_responses.py +679 -0
- aworg/packages.py +261 -0
- aworg/panes.py +519 -0
- aworg/paths.py +188 -0
- aworg/personas/atelier/LICENSE +18 -0
- aworg/personas/atelier/PERSONA.md +55 -0
- aworg/personas/atelier/avatar.svg +8 -0
- aworg/personas/atelier/background.svg +28 -0
- aworg/personas/atelier/theme.json +17 -0
- aworg/personas/aworg-dark/LICENSE +18 -0
- aworg/personas/aworg-dark/PERSONA.md +45 -0
- aworg/personas/aworg-dark/avatar.svg +36 -0
- aworg/personas/aworg-dark/background.svg +241 -0
- aworg/personas/aworg-dark/theme.json +17 -0
- aworg/personas/aworg-light/LICENSE +18 -0
- aworg/personas/aworg-light/PERSONA.md +45 -0
- aworg/personas/aworg-light/avatar.svg +36 -0
- aworg/personas/aworg-light/background.svg +241 -0
- aworg/personas/aworg-light/theme.json +17 -0
- aworg/personas/egirl/LICENSE +18 -0
- aworg/personas/egirl/PERSONA.md +46 -0
- aworg/personas/egirl/avatar.svg +21 -0
- aworg/personas/egirl/background.svg +146 -0
- aworg/personas/egirl/theme.json +17 -0
- aworg/personas/player-two/LICENSE +18 -0
- aworg/personas/player-two/PERSONA.md +60 -0
- aworg/personas/player-two/avatar.svg +13 -0
- aworg/personas/player-two/background.svg +123 -0
- aworg/personas/player-two/theme.json +17 -0
- aworg/personas/professional/LICENSE +18 -0
- aworg/personas/professional/PERSONA.md +59 -0
- aworg/personas/professional/avatar.svg +15 -0
- aworg/personas/professional/background.svg +50 -0
- aworg/personas/professional/theme.json +17 -0
- aworg/personas/sunny/LICENSE +18 -0
- aworg/personas/sunny/PERSONA.md +57 -0
- aworg/personas/sunny/avatar.svg +18 -0
- aworg/personas/sunny/background.svg +91 -0
- aworg/personas/sunny/theme.json +15 -0
- aworg/personas.py +700 -0
- aworg/processes.py +442 -0
- aworg/providers.py +223 -0
- aworg/ratelimit.py +320 -0
- aworg/resident.py +1044 -0
- aworg/secrets.py +89 -0
- aworg/server.py +1990 -0
- aworg/skills.py +413 -0
- aworg/stopping.py +65 -0
- aworg/storage.py +1399 -0
- aworg/theme.py +370 -0
- aworg/tools/__init__.py +18 -0
- aworg/tools/application/__init__.py +21 -0
- aworg/tools/application/name_project.py +97 -0
- aworg/tools/application/reload_preview.py +63 -0
- aworg/tools/base.py +288 -0
- aworg/tools/filesystem/__init__.py +9 -0
- aworg/tools/filesystem/_paths.py +102 -0
- aworg/tools/filesystem/copy_file.py +82 -0
- aworg/tools/filesystem/delete_file.py +107 -0
- aworg/tools/filesystem/edit_file.py +150 -0
- aworg/tools/filesystem/make_directory.py +81 -0
- aworg/tools/filesystem/move_file.py +87 -0
- aworg/tools/filesystem/read_file.py +136 -0
- aworg/tools/filesystem/read_image.py +183 -0
- aworg/tools/filesystem/search_files.py +185 -0
- aworg/tools/filesystem/write_file.py +97 -0
- aworg/tools/http/__init__.py +5 -0
- aworg/tools/http/http_request.py +181 -0
- aworg/tools/journal/__init__.py +13 -0
- aworg/tools/journal/list_concerns.py +73 -0
- aworg/tools/journal/resolve_concern.py +98 -0
- aworg/tools/knowledge/__init__.py +10 -0
- aworg/tools/knowledge/list_skills.py +75 -0
- aworg/tools/knowledge/read_skill.py +108 -0
- aworg/tools/planning/__init__.py +11 -0
- aworg/tools/planning/add_tasks.py +128 -0
- aworg/tools/planning/list_tasks.py +77 -0
- aworg/tools/planning/update_task.py +107 -0
- aworg/tools/registry.py +547 -0
- aworg/tools/shell/__init__.py +5 -0
- aworg/tools/shell/execute_command.py +202 -0
- aworg/tools/shell/list_processes.py +81 -0
- aworg/tools/shell/start_process.py +170 -0
- aworg/tools/shell/stop_process.py +46 -0
- aworg/tools/workers/__init__.py +6 -0
- aworg/tools/workers/check_workers.py +68 -0
- aworg/tools/workers/message_worker.py +37 -0
- aworg/tools/workers/spawn_worker.py +163 -0
- aworg/tools/workers/stop_worker.py +35 -0
- aworg/watch.py +172 -0
- aworg/web/app.js +4306 -0
- aworg/web/highlight.js +215 -0
- aworg/web/index.html +421 -0
- aworg/web/markdown.js +171 -0
- aworg/web/style.css +2675 -0
- aworg/workers.py +402 -0
- aworg-1.0.0.dist-info/METADATA +944 -0
- aworg-1.0.0.dist-info/RECORD +119 -0
- aworg-1.0.0.dist-info/WHEEL +5 -0
- aworg-1.0.0.dist-info/entry_points.txt +2 -0
- aworg-1.0.0.dist-info/licenses/ADDITIONAL-TERMS.md +58 -0
- aworg-1.0.0.dist-info/licenses/LICENSE +101 -0
- aworg-1.0.0.dist-info/licenses/TRADEMARKS.md +33 -0
- aworg-1.0.0.dist-info/top_level.txt +1 -0
aworg/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""AWORG - Autonomous Workspace Organism.
|
|
2
|
+
|
|
3
|
+
A persistent AI Resident that inhabits this machine.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version as _installed_version
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
#: What this build calls itself.
|
|
10
|
+
#:
|
|
11
|
+
#: Read back from the installed distribution rather than written down here,
|
|
12
|
+
#: because it was written down in three places -- pyproject.toml, this file,
|
|
13
|
+
#: and a hardcoded string in server.py's FastAPI() -- none of which read the
|
|
14
|
+
#: others. They agreed by luck. The first release that bumped two of the
|
|
15
|
+
#: three would have had the interface confidently reporting a version that
|
|
16
|
+
#: was not the one running, which is precisely the kind of claim the
|
|
17
|
+
#: Environment pane exists to make checkable.
|
|
18
|
+
#:
|
|
19
|
+
#: pyproject.toml is where a Python project's version belongs and the only
|
|
20
|
+
#: place this number is written down. Everything else -- this attribute, the
|
|
21
|
+
#: API's version field, the Environment pane, the Resident's own prompt --
|
|
22
|
+
#: reads it back from the metadata that installing actually produced.
|
|
23
|
+
#:
|
|
24
|
+
#: "unknown" rather than a literal when there is no metadata to ask, which
|
|
25
|
+
#: happens only when running out of a checkout nobody installed. A hardcoded
|
|
26
|
+
#: fallback would be a second source of truth that is right until the day it
|
|
27
|
+
#: is not, and would then report a wrong version confidently in the one place
|
|
28
|
+
#: an owner would go to check. The whole reason this is on screen is so a
|
|
29
|
+
#: claim about what is running can be trusted; "unknown" keeps that true.
|
|
30
|
+
try:
|
|
31
|
+
__version__ = _installed_version("aworg")
|
|
32
|
+
except PackageNotFoundError: # pragma: no cover - only outside an install
|
|
33
|
+
__version__ = "unknown"
|
aworg/__main__.py
ADDED
aworg/activities.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Activities: what AWORG is doing right now.
|
|
2
|
+
|
|
3
|
+
Three different questions, kept apart deliberately:
|
|
4
|
+
|
|
5
|
+
Tasks what should be accomplished
|
|
6
|
+
Activities what is happening at this moment
|
|
7
|
+
Living Log what happened, and mattered
|
|
8
|
+
|
|
9
|
+
An Activity is a runtime object rather than a callback. Something starts one,
|
|
10
|
+
it moves through a lifecycle, and it emits an event at every transition.
|
|
11
|
+
Whoever cares subscribes. The manager itself decides nothing -- not what to
|
|
12
|
+
run next, not what is worth remembering. It tracks and it emits; subscribers
|
|
13
|
+
decide what that means.
|
|
14
|
+
|
|
15
|
+
That separation is the whole design. A tool call that notified the interface
|
|
16
|
+
directly, and the Living Log directly, and its caller directly, would have to
|
|
17
|
+
know about all three. It knows about none of them, and gains a fourth
|
|
18
|
+
subscriber without being touched.
|
|
19
|
+
|
|
20
|
+
Activities are runtime state and do not survive a restart. What the model was
|
|
21
|
+
shown lives in the conversation, which is permanent; an Activity holds the
|
|
22
|
+
full evidence behind that while the Aworg is up. Losing evidence depth on
|
|
23
|
+
restart is a real cost, and the fix is persistence rather than moving the
|
|
24
|
+
payload into the conversation -- the conversation records what was said.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import asyncio
|
|
30
|
+
import itertools
|
|
31
|
+
import time
|
|
32
|
+
import uuid
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
#: Lifecycle states. The first four are the ordinary path; the last four are
|
|
37
|
+
#: terminal, and three of them are ways of not succeeding.
|
|
38
|
+
CREATED = "created"
|
|
39
|
+
QUEUED = "queued"
|
|
40
|
+
RUNNING = "running"
|
|
41
|
+
WAITING = "waiting"
|
|
42
|
+
COMPLETED = "completed"
|
|
43
|
+
FAILED = "failed"
|
|
44
|
+
CANCELLED = "cancelled"
|
|
45
|
+
TIMED_OUT = "timed_out"
|
|
46
|
+
|
|
47
|
+
TERMINAL = frozenset({COMPLETED, FAILED, CANCELLED, TIMED_OUT})
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Activity:
|
|
51
|
+
"""One piece of work, observable while it happens.
|
|
52
|
+
|
|
53
|
+
Carries two records of its result, and the difference between them is the
|
|
54
|
+
point. `summary` is what the caller was shown -- sized to fit a context
|
|
55
|
+
window, and stored in the conversation because it is what was said.
|
|
56
|
+
`payload` is the whole of what actually came back, kept here as evidence
|
|
57
|
+
for an owner who wants to see for themselves.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
kind: str,
|
|
63
|
+
label: str,
|
|
64
|
+
source: str = "resident",
|
|
65
|
+
parent_id: str | None = None,
|
|
66
|
+
detail: str = "",
|
|
67
|
+
):
|
|
68
|
+
self.id = uuid.uuid4().hex[:12]
|
|
69
|
+
#: What sort of work this is -- "tool" today, more later. The
|
|
70
|
+
#: interface groups on it; nothing branches on it.
|
|
71
|
+
self.kind = kind
|
|
72
|
+
#: Short human phrase, shown in the Activities panel as-is.
|
|
73
|
+
self.label = label
|
|
74
|
+
#: Who owns this: "resident", or a worker's id once workers exist.
|
|
75
|
+
self.source = source
|
|
76
|
+
self.parent_id = parent_id
|
|
77
|
+
self.detail = detail
|
|
78
|
+
|
|
79
|
+
self.state = CREATED
|
|
80
|
+
self.created_at = time.time()
|
|
81
|
+
self.started_at: float | None = None
|
|
82
|
+
self.ended_at: float | None = None
|
|
83
|
+
#: 0.0-1.0 where a job can say, None where it cannot. Most cannot,
|
|
84
|
+
#: and inventing a number for them would be a progress bar that lies.
|
|
85
|
+
self.progress: float | None = None
|
|
86
|
+
self.summary: str = ""
|
|
87
|
+
self.payload: Any = None
|
|
88
|
+
self.error: str | None = None
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def duration(self) -> float | None:
|
|
92
|
+
if self.started_at is None:
|
|
93
|
+
return None
|
|
94
|
+
return (self.ended_at or time.time()) - self.started_at
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def finished(self) -> bool:
|
|
98
|
+
return self.state in TERMINAL
|
|
99
|
+
|
|
100
|
+
def snapshot(self) -> dict[str, Any]:
|
|
101
|
+
"""What a subscriber sees. Never the payload: it may be enormous.
|
|
102
|
+
|
|
103
|
+
The interface asks for a payload by id when the owner opens one,
|
|
104
|
+
rather than every subscriber carrying megabytes of command output
|
|
105
|
+
through a websocket to display forty characters of it.
|
|
106
|
+
"""
|
|
107
|
+
return {
|
|
108
|
+
"id": self.id,
|
|
109
|
+
"kind": self.kind,
|
|
110
|
+
"label": self.label,
|
|
111
|
+
"source": self.source,
|
|
112
|
+
"parent_id": self.parent_id,
|
|
113
|
+
"detail": self.detail,
|
|
114
|
+
"state": self.state,
|
|
115
|
+
"progress": self.progress,
|
|
116
|
+
"summary": self.summary,
|
|
117
|
+
"error": self.error,
|
|
118
|
+
"duration": self.duration,
|
|
119
|
+
"created_at": self.created_at,
|
|
120
|
+
"has_payload": self.payload is not None,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ActivityManager:
|
|
125
|
+
"""Tracks Activities and announces what happens to them.
|
|
126
|
+
|
|
127
|
+
Deliberately without opinions. It will not start work, will not decide
|
|
128
|
+
what runs next, and will not judge that something deserves a place in the
|
|
129
|
+
Living Log. Every one of those is a subscriber's job, and keeping them out
|
|
130
|
+
of here is what stops this becoming the place all behaviour accumulates.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
#: How many finished Activities to keep. Running ones are never dropped
|
|
134
|
+
#: however many there are -- a cap that could evict live work would make
|
|
135
|
+
#: the panel lie about what is happening.
|
|
136
|
+
HISTORY = 200
|
|
137
|
+
|
|
138
|
+
def __init__(self) -> None:
|
|
139
|
+
self._activities: dict[str, Activity] = {}
|
|
140
|
+
self._order: list[str] = []
|
|
141
|
+
self._subscribers: set[asyncio.Queue] = set()
|
|
142
|
+
self._counter = itertools.count()
|
|
143
|
+
|
|
144
|
+
# -- lifecycle ------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
def create(
|
|
147
|
+
self,
|
|
148
|
+
kind: str,
|
|
149
|
+
label: str,
|
|
150
|
+
source: str = "resident",
|
|
151
|
+
parent_id: str | None = None,
|
|
152
|
+
detail: str = "",
|
|
153
|
+
) -> Activity:
|
|
154
|
+
activity = Activity(kind, label, source, parent_id, detail)
|
|
155
|
+
self._activities[activity.id] = activity
|
|
156
|
+
self._order.append(activity.id)
|
|
157
|
+
self._prune()
|
|
158
|
+
self._emit("created", activity)
|
|
159
|
+
return activity
|
|
160
|
+
|
|
161
|
+
def queued(self, activity: Activity) -> None:
|
|
162
|
+
self._transition(activity, QUEUED, "queued")
|
|
163
|
+
|
|
164
|
+
def started(self, activity: Activity) -> None:
|
|
165
|
+
activity.started_at = time.time()
|
|
166
|
+
self._transition(activity, RUNNING, "started")
|
|
167
|
+
|
|
168
|
+
def waiting(self, activity: Activity, detail: str = "") -> None:
|
|
169
|
+
if detail:
|
|
170
|
+
activity.detail = detail
|
|
171
|
+
self._transition(activity, WAITING, "waiting")
|
|
172
|
+
|
|
173
|
+
def progress(
|
|
174
|
+
self, activity: Activity, fraction: float | None = None, detail: str = ""
|
|
175
|
+
) -> None:
|
|
176
|
+
"""A running job saying it has got somewhere.
|
|
177
|
+
|
|
178
|
+
Not a state change -- an Activity reporting progress is still
|
|
179
|
+
running -- so this emits without touching `state`.
|
|
180
|
+
"""
|
|
181
|
+
if fraction is not None:
|
|
182
|
+
activity.progress = max(0.0, min(1.0, fraction))
|
|
183
|
+
if detail:
|
|
184
|
+
activity.detail = detail
|
|
185
|
+
self._emit("progress", activity)
|
|
186
|
+
|
|
187
|
+
def completed(self, activity: Activity, summary: str = "", payload: Any = None) -> None:
|
|
188
|
+
activity.summary = summary
|
|
189
|
+
activity.payload = payload
|
|
190
|
+
self._finish(activity, COMPLETED, "completed")
|
|
191
|
+
|
|
192
|
+
def failed(self, activity: Activity, error: str, payload: Any = None) -> None:
|
|
193
|
+
activity.error = error
|
|
194
|
+
activity.payload = payload
|
|
195
|
+
self._finish(activity, FAILED, "failed")
|
|
196
|
+
|
|
197
|
+
def cancelled(self, activity: Activity) -> None:
|
|
198
|
+
self._finish(activity, CANCELLED, "cancelled")
|
|
199
|
+
|
|
200
|
+
def timed_out(self, activity: Activity, error: str = "") -> None:
|
|
201
|
+
activity.error = error or "Timed out."
|
|
202
|
+
self._finish(activity, TIMED_OUT, "timed_out")
|
|
203
|
+
|
|
204
|
+
def _finish(self, activity: Activity, state: str, event: str) -> None:
|
|
205
|
+
activity.ended_at = time.time()
|
|
206
|
+
self._transition(activity, state, event)
|
|
207
|
+
|
|
208
|
+
def _transition(self, activity: Activity, state: str, event: str) -> None:
|
|
209
|
+
activity.state = state
|
|
210
|
+
self._emit(event, activity)
|
|
211
|
+
|
|
212
|
+
# -- subscription ---------------------------------------------------
|
|
213
|
+
|
|
214
|
+
def _emit(self, event: str, activity: Activity) -> None:
|
|
215
|
+
message = {
|
|
216
|
+
"seq": next(self._counter),
|
|
217
|
+
"event": event,
|
|
218
|
+
"activity": activity.snapshot(),
|
|
219
|
+
}
|
|
220
|
+
for queue in list(self._subscribers):
|
|
221
|
+
queue.put_nowait(message)
|
|
222
|
+
|
|
223
|
+
def subscribe(self) -> asyncio.Queue:
|
|
224
|
+
queue: asyncio.Queue = asyncio.Queue()
|
|
225
|
+
self._subscribers.add(queue)
|
|
226
|
+
return queue
|
|
227
|
+
|
|
228
|
+
def unsubscribe(self, queue: asyncio.Queue) -> None:
|
|
229
|
+
self._subscribers.discard(queue)
|
|
230
|
+
|
|
231
|
+
# -- reading --------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
def get(self, activity_id: str) -> Activity | None:
|
|
234
|
+
return self._activities.get(activity_id)
|
|
235
|
+
|
|
236
|
+
def live(self) -> list[dict[str, Any]]:
|
|
237
|
+
"""Everything not yet finished, oldest first."""
|
|
238
|
+
return [
|
|
239
|
+
self._activities[i].snapshot()
|
|
240
|
+
for i in self._order
|
|
241
|
+
if not self._activities[i].finished
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
def recent(self, limit: int = 50) -> list[dict[str, Any]]:
|
|
245
|
+
"""The latest Activities whatever their state, newest last."""
|
|
246
|
+
chosen = self._order[-limit:]
|
|
247
|
+
return [self._activities[i].snapshot() for i in chosen]
|
|
248
|
+
|
|
249
|
+
def children(self, parent_id: str) -> list[Activity]:
|
|
250
|
+
return [
|
|
251
|
+
self._activities[i]
|
|
252
|
+
for i in self._order
|
|
253
|
+
if self._activities[i].parent_id == parent_id
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
def forget_all(self) -> None:
|
|
257
|
+
"""Drop every Activity, keeping every subscriber.
|
|
258
|
+
|
|
259
|
+
For a reset, and the distinction is the whole point of the method
|
|
260
|
+
existing. Replacing the manager wholesale is the obvious way to clear
|
|
261
|
+
it and it is silently wrong: everything already subscribed keeps
|
|
262
|
+
holding a queue on the old object and never hears from the new one.
|
|
263
|
+
|
|
264
|
+
That happened. A reset left the Living Log's follower watching a
|
|
265
|
+
manager nothing would ever emit on again, so an Aworg went quiet the
|
|
266
|
+
moment it was reset -- workers ran, tools failed, and none of it was
|
|
267
|
+
written down. The interface survived only because its stream
|
|
268
|
+
reconnects on its own and re-subscribes to whatever is there now.
|
|
269
|
+
|
|
270
|
+
Subscribers are deliberately not told. A cleared manager has no
|
|
271
|
+
events to describe, and an invented one would put a lifecycle
|
|
272
|
+
transition on the wire for work that no longer exists.
|
|
273
|
+
"""
|
|
274
|
+
self._activities.clear()
|
|
275
|
+
self._order.clear()
|
|
276
|
+
|
|
277
|
+
def _prune(self) -> None:
|
|
278
|
+
"""Forget the oldest finished Activities once there are too many.
|
|
279
|
+
|
|
280
|
+
Running work is skipped rather than counted, so a burst of concurrent
|
|
281
|
+
jobs cannot push a live one out of the panel it is meant to appear in.
|
|
282
|
+
"""
|
|
283
|
+
finished = [i for i in self._order if self._activities[i].finished]
|
|
284
|
+
excess = len(finished) - self.HISTORY
|
|
285
|
+
for activity_id in finished[:max(0, excess)]:
|
|
286
|
+
self._activities.pop(activity_id, None)
|
|
287
|
+
self._order.remove(activity_id)
|
|
288
|
+
|