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.
Files changed (119) hide show
  1. aworg/__init__.py +33 -0
  2. aworg/__main__.py +6 -0
  3. aworg/activities.py +288 -0
  4. aworg/agent.py +621 -0
  5. aworg/auth.py +183 -0
  6. aworg/cli.py +372 -0
  7. aworg/host.py +382 -0
  8. aworg/inbox.py +112 -0
  9. aworg/install.py +502 -0
  10. aworg/journal.py +448 -0
  11. aworg/layout.py +199 -0
  12. aworg/models/__init__.py +102 -0
  13. aworg/models/anthropic.py +257 -0
  14. aworg/models/base.py +319 -0
  15. aworg/models/openai_compatible.py +649 -0
  16. aworg/models/openai_responses.py +679 -0
  17. aworg/packages.py +261 -0
  18. aworg/panes.py +519 -0
  19. aworg/paths.py +188 -0
  20. aworg/personas/atelier/LICENSE +18 -0
  21. aworg/personas/atelier/PERSONA.md +55 -0
  22. aworg/personas/atelier/avatar.svg +8 -0
  23. aworg/personas/atelier/background.svg +28 -0
  24. aworg/personas/atelier/theme.json +17 -0
  25. aworg/personas/aworg-dark/LICENSE +18 -0
  26. aworg/personas/aworg-dark/PERSONA.md +45 -0
  27. aworg/personas/aworg-dark/avatar.svg +36 -0
  28. aworg/personas/aworg-dark/background.svg +241 -0
  29. aworg/personas/aworg-dark/theme.json +17 -0
  30. aworg/personas/aworg-light/LICENSE +18 -0
  31. aworg/personas/aworg-light/PERSONA.md +45 -0
  32. aworg/personas/aworg-light/avatar.svg +36 -0
  33. aworg/personas/aworg-light/background.svg +241 -0
  34. aworg/personas/aworg-light/theme.json +17 -0
  35. aworg/personas/egirl/LICENSE +18 -0
  36. aworg/personas/egirl/PERSONA.md +46 -0
  37. aworg/personas/egirl/avatar.svg +21 -0
  38. aworg/personas/egirl/background.svg +146 -0
  39. aworg/personas/egirl/theme.json +17 -0
  40. aworg/personas/player-two/LICENSE +18 -0
  41. aworg/personas/player-two/PERSONA.md +60 -0
  42. aworg/personas/player-two/avatar.svg +13 -0
  43. aworg/personas/player-two/background.svg +123 -0
  44. aworg/personas/player-two/theme.json +17 -0
  45. aworg/personas/professional/LICENSE +18 -0
  46. aworg/personas/professional/PERSONA.md +59 -0
  47. aworg/personas/professional/avatar.svg +15 -0
  48. aworg/personas/professional/background.svg +50 -0
  49. aworg/personas/professional/theme.json +17 -0
  50. aworg/personas/sunny/LICENSE +18 -0
  51. aworg/personas/sunny/PERSONA.md +57 -0
  52. aworg/personas/sunny/avatar.svg +18 -0
  53. aworg/personas/sunny/background.svg +91 -0
  54. aworg/personas/sunny/theme.json +15 -0
  55. aworg/personas.py +700 -0
  56. aworg/processes.py +442 -0
  57. aworg/providers.py +223 -0
  58. aworg/ratelimit.py +320 -0
  59. aworg/resident.py +1044 -0
  60. aworg/secrets.py +89 -0
  61. aworg/server.py +1990 -0
  62. aworg/skills.py +413 -0
  63. aworg/stopping.py +65 -0
  64. aworg/storage.py +1399 -0
  65. aworg/theme.py +370 -0
  66. aworg/tools/__init__.py +18 -0
  67. aworg/tools/application/__init__.py +21 -0
  68. aworg/tools/application/name_project.py +97 -0
  69. aworg/tools/application/reload_preview.py +63 -0
  70. aworg/tools/base.py +288 -0
  71. aworg/tools/filesystem/__init__.py +9 -0
  72. aworg/tools/filesystem/_paths.py +102 -0
  73. aworg/tools/filesystem/copy_file.py +82 -0
  74. aworg/tools/filesystem/delete_file.py +107 -0
  75. aworg/tools/filesystem/edit_file.py +150 -0
  76. aworg/tools/filesystem/make_directory.py +81 -0
  77. aworg/tools/filesystem/move_file.py +87 -0
  78. aworg/tools/filesystem/read_file.py +136 -0
  79. aworg/tools/filesystem/read_image.py +183 -0
  80. aworg/tools/filesystem/search_files.py +185 -0
  81. aworg/tools/filesystem/write_file.py +97 -0
  82. aworg/tools/http/__init__.py +5 -0
  83. aworg/tools/http/http_request.py +181 -0
  84. aworg/tools/journal/__init__.py +13 -0
  85. aworg/tools/journal/list_concerns.py +73 -0
  86. aworg/tools/journal/resolve_concern.py +98 -0
  87. aworg/tools/knowledge/__init__.py +10 -0
  88. aworg/tools/knowledge/list_skills.py +75 -0
  89. aworg/tools/knowledge/read_skill.py +108 -0
  90. aworg/tools/planning/__init__.py +11 -0
  91. aworg/tools/planning/add_tasks.py +128 -0
  92. aworg/tools/planning/list_tasks.py +77 -0
  93. aworg/tools/planning/update_task.py +107 -0
  94. aworg/tools/registry.py +547 -0
  95. aworg/tools/shell/__init__.py +5 -0
  96. aworg/tools/shell/execute_command.py +202 -0
  97. aworg/tools/shell/list_processes.py +81 -0
  98. aworg/tools/shell/start_process.py +170 -0
  99. aworg/tools/shell/stop_process.py +46 -0
  100. aworg/tools/workers/__init__.py +6 -0
  101. aworg/tools/workers/check_workers.py +68 -0
  102. aworg/tools/workers/message_worker.py +37 -0
  103. aworg/tools/workers/spawn_worker.py +163 -0
  104. aworg/tools/workers/stop_worker.py +35 -0
  105. aworg/watch.py +172 -0
  106. aworg/web/app.js +4306 -0
  107. aworg/web/highlight.js +215 -0
  108. aworg/web/index.html +421 -0
  109. aworg/web/markdown.js +171 -0
  110. aworg/web/style.css +2675 -0
  111. aworg/workers.py +402 -0
  112. aworg-1.0.0.dist-info/METADATA +944 -0
  113. aworg-1.0.0.dist-info/RECORD +119 -0
  114. aworg-1.0.0.dist-info/WHEEL +5 -0
  115. aworg-1.0.0.dist-info/entry_points.txt +2 -0
  116. aworg-1.0.0.dist-info/licenses/ADDITIONAL-TERMS.md +58 -0
  117. aworg-1.0.0.dist-info/licenses/LICENSE +101 -0
  118. aworg-1.0.0.dist-info/licenses/TRADEMARKS.md +33 -0
  119. 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
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
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
+