taskuary 0.2.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.
- taskuary/__init__.py +2 -0
- taskuary/agents.py +218 -0
- taskuary/artifacts.py +209 -0
- taskuary/aws.py +226 -0
- taskuary/azure.py +334 -0
- taskuary/blackboard.py +150 -0
- taskuary/channels.py +768 -0
- taskuary/ci.py +237 -0
- taskuary/cli.py +47 -0
- taskuary/coder.py +132 -0
- taskuary/config.py +62 -0
- taskuary/db.py +63 -0
- taskuary/desktop.py +72 -0
- taskuary/devtools.py +352 -0
- taskuary/digest.py +104 -0
- taskuary/docsync.py +124 -0
- taskuary/github.py +136 -0
- taskuary/histgen.py +220 -0
- taskuary/imapmail.py +150 -0
- taskuary/ingest.py +344 -0
- taskuary/learn.py +181 -0
- taskuary/llm.py +175 -0
- taskuary/logs.py +17 -0
- taskuary/mcp.py +78 -0
- taskuary/messengers.py +205 -0
- taskuary/mssql.py +64 -0
- taskuary/outbound.py +221 -0
- taskuary/phone.py +88 -0
- taskuary/pm.py +310 -0
- taskuary/policy.py +59 -0
- taskuary/proof.py +194 -0
- taskuary/proposals.py +132 -0
- taskuary/reports.py +508 -0
- taskuary/reshape.py +201 -0
- taskuary/responder.py +199 -0
- taskuary/routing.py +115 -0
- taskuary/scopes.py +88 -0
- taskuary/server.py +1506 -0
- taskuary/store.py +711 -0
- taskuary/templates/coder.md +35 -0
- taskuary/templates/digest.md +5 -0
- taskuary/templates/learned.md +31 -0
- taskuary/templates/soul.md +44 -0
- taskuary/templates/style.md +14 -0
- taskuary/templates/triage.md +22 -0
- taskuary/terminal.py +971 -0
- taskuary/toil.py +57 -0
- taskuary/triage.py +123 -0
- taskuary/verdicts.py +73 -0
- taskuary/web/assets/index-6GBZ9nXN.css +32 -0
- taskuary/web/assets/index-Cjj87C2X.js +401 -0
- taskuary/web/favicon.ico +0 -0
- taskuary/web/favicon.png +0 -0
- taskuary/web/index.html +25 -0
- taskuary/whatsapp/bridge.mjs +101 -0
- taskuary/whatsapp/package.json +11 -0
- taskuary-0.2.0.dist-info/METADATA +424 -0
- taskuary-0.2.0.dist-info/RECORD +62 -0
- taskuary-0.2.0.dist-info/WHEEL +5 -0
- taskuary-0.2.0.dist-info/entry_points.txt +3 -0
- taskuary-0.2.0.dist-info/licenses/LICENSE +21 -0
- taskuary-0.2.0.dist-info/top_level.txt +1 -0
taskuary/blackboard.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Agents that know about each other. The board IS the blackboard: who is working which
|
|
2
|
+
task in which checkout, and what files each has actually modified so far - read off git and
|
|
3
|
+
the run trace, never off a plan (agents predicting their own scope get it wrong; their
|
|
4
|
+
tracks do not). A new agent in the SAME checkout gets that picture in its seed prompt (a
|
|
5
|
+
peer in another repo is none of its business, and its tokens); a task LIKELY to collide
|
|
6
|
+
queues behind the one already working - first agent in has control.
|
|
7
|
+
"""
|
|
8
|
+
import json, os, re, threading, time
|
|
9
|
+
from loguru import logger
|
|
10
|
+
|
|
11
|
+
from .store import task_ref
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def norm(p): return os.path.normcase(os.path.normpath(p or ''))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dirty(cwd) -> set:
|
|
18
|
+
"""Uncommitted paths in a checkout, per git itself."""
|
|
19
|
+
from .agents import _git
|
|
20
|
+
return {l[3:].strip().strip('"') for l in _git(cwd, 'status', '--porcelain').splitlines() if len(l) > 3}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# what a headless run touched, read off its own live trace ("→ Edit: path" lines)
|
|
24
|
+
_TOUCH = re.compile(r'^→ (?:Edit|Write|MultiEdit|NotebookEdit)\w*: (.+)$', re.M)
|
|
25
|
+
|
|
26
|
+
def trace_files(trace_json: str) -> list:
|
|
27
|
+
try: evs = json.loads(trace_json or '[]')
|
|
28
|
+
except ValueError: return []
|
|
29
|
+
seen = []
|
|
30
|
+
for e in evs:
|
|
31
|
+
if e.get('kind') != 'live': continue
|
|
32
|
+
for m in _TOUCH.finditer(str(e.get('detail') or '')):
|
|
33
|
+
p = m.group(1).strip()
|
|
34
|
+
if p and p not in seen: seen.append(p)
|
|
35
|
+
return seen[:20]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def target_cwd(store, tid: int, agent: str) -> str:
|
|
39
|
+
"""Where WOULD this task's session open? guess_repo + the profile's paths, minus the disk
|
|
40
|
+
search open_session may still run - close enough for routing, never used to refuse."""
|
|
41
|
+
from . import terminal as term
|
|
42
|
+
row = store.get_agent(agent) or {}
|
|
43
|
+
prof = json.loads(row.get('Config') or '{}')
|
|
44
|
+
try: repo, _ = term.guess_repo(store, tid, prof)
|
|
45
|
+
except Exception: repo = None
|
|
46
|
+
return (prof.get('cwd_map') or {}).get(repo or '') or prof.get('cwd') or os.getcwd()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def peers(store, cwd, exclude_tid=None) -> list:
|
|
50
|
+
"""Who else is on THIS checkout right now - live pty sessions and headless runs:
|
|
51
|
+
[{tid, ref, title, agent, files, started}]."""
|
|
52
|
+
from . import terminal as term
|
|
53
|
+
out, me = [], norm(cwd)
|
|
54
|
+
for t in term.SESSIONS.values():
|
|
55
|
+
if t.alive and t.task_id and t.task_id != exclude_tid and norm(t.cwd) == me:
|
|
56
|
+
task = store.get_task(t.task_id) or {}
|
|
57
|
+
out.append({'tid': t.task_id, 'ref': task_ref(t.task_id), 'title': task.get('Title') or '',
|
|
58
|
+
'agent': t.agent or t.label, 'files': t.files(), 'started': t.started})
|
|
59
|
+
for r in store.running_runs():
|
|
60
|
+
if not r.get('TaskId') or r['TaskId'] == exclude_tid or any(p['tid'] == r['TaskId'] for p in out): continue
|
|
61
|
+
prof = json.loads((store.get_agent(r['AgentName']) or {}).get('Config') or '{}')
|
|
62
|
+
if prof.get('cwd') and norm(prof['cwd']) == me:
|
|
63
|
+
task = store.get_task(r['TaskId']) or {}
|
|
64
|
+
out.append({'tid': r['TaskId'], 'ref': task_ref(r['TaskId']), 'title': task.get('Title') or '',
|
|
65
|
+
'agent': r['AgentName'], 'files': trace_files(r.get('TraceJson')), 'started': r.get('StartedAt')})
|
|
66
|
+
return out
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def briefing(store, cwd, exclude_tid=None) -> str:
|
|
70
|
+
"""The OTHER AGENTS paragraph of a new agent's prompt. Facts only - task, agent, the files
|
|
71
|
+
it has modified so far - plus the one standing rule: whoever got there first has control."""
|
|
72
|
+
ps = peers(store, cwd, exclude_tid)
|
|
73
|
+
if not ps: return ''
|
|
74
|
+
who = ' | '.join(f"{p['ref']} \"{p['title'][:80]}\" ({p['agent']}) - files it has modified so far: "
|
|
75
|
+
+ (', '.join(os.path.basename(x) for x in p['files'][:10]) or 'none yet') for p in ps)
|
|
76
|
+
return ('OTHER AGENTS are working in this same checkout RIGHT NOW: ' + who + '. They were here '
|
|
77
|
+
'first and have control of their files: never edit, revert, stash or commit them, and '
|
|
78
|
+
'never use git add -A / git commit -a - stage and commit ONLY files you yourself changed '
|
|
79
|
+
'(git status before committing; uncommitted changes that are not yours are their work in '
|
|
80
|
+
'progress, leave them exactly as they are).')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
OVERLAP_SYSTEM = (
|
|
84
|
+
'Agents work coding tasks in the same git checkout. A new task is about to start; some are '
|
|
85
|
+
'already running. Would the new task likely modify any of the SAME FILES as a running one? '
|
|
86
|
+
'Judge from the descriptions and the files already touched. Output ONLY this JSON: '
|
|
87
|
+
'{"overlap": true/false, "with": "TQ-nnnn or empty", "why": "one short sentence"} - '
|
|
88
|
+
'when unsure, say false: a wrong true only delays work, and the agents are told about '
|
|
89
|
+
'each other either way.')
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def likely_overlap(store, tid: int, ps: list) -> tuple:
|
|
93
|
+
"""(peer, why) when the new task would probably collide with a running one, else (None, '').
|
|
94
|
+
Routing only - a wrong yes waits minutes, a wrong no still gets the briefing - so no AI
|
|
95
|
+
configured (or a bad answer) simply starts the task."""
|
|
96
|
+
from .llm import build_llm
|
|
97
|
+
t = store.get_task(tid) or {}
|
|
98
|
+
try:
|
|
99
|
+
llm = build_llm(store)
|
|
100
|
+
if not llm: return None, ''
|
|
101
|
+
running = '\n'.join(f"- {p['ref']} \"{p['title']}\" - files so far: "
|
|
102
|
+
+ (', '.join(p['files'][:15]) or 'none yet') for p in ps)
|
|
103
|
+
out = llm(OVERLAP_SYSTEM, f"New task: \"{t.get('Title') or ''}\" - {str(t.get('Summary') or '')[:1500]}\n\n"
|
|
104
|
+
f'Running in the same checkout:\n{running}', max_tokens=200)
|
|
105
|
+
j = json.loads(re.sub(r'^```(json)?|```$', '', (out or '').strip(), flags=re.M))
|
|
106
|
+
if j.get('overlap'):
|
|
107
|
+
return next((p for p in ps if p['ref'] == str(j.get('with') or '')), ps[0]), str(j.get('why') or '')
|
|
108
|
+
except Exception as e:
|
|
109
|
+
logger.debug(f'overlap check failed for task {tid}: {e}')
|
|
110
|
+
return None, ''
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
_DRAINING = threading.Lock()
|
|
114
|
+
|
|
115
|
+
def drain(store):
|
|
116
|
+
"""A session ended (or a slot freed up): start what was queued, in arrival order. Anything
|
|
117
|
+
whose blocker is still working stays put; anything whose task moved on is just cleared."""
|
|
118
|
+
from . import terminal as term
|
|
119
|
+
from .ingest import AUTO_SESSIONS
|
|
120
|
+
if not _DRAINING.acquire(blocking=False): return
|
|
121
|
+
try:
|
|
122
|
+
for q in store.queued_dispatches():
|
|
123
|
+
if len([t for t in term.SESSIONS.values() if t.alive]) >= AUTO_SESSIONS: return
|
|
124
|
+
b = q.get('BehindTaskId')
|
|
125
|
+
if b and (term.for_task(b) or any(r['TaskId'] == b for r in store.running_runs())): continue
|
|
126
|
+
t = store.get_task(q['TaskId']) or {}
|
|
127
|
+
if t.get('Status') not in ('open', 'in_progress') or term.for_task(q['TaskId']):
|
|
128
|
+
store.clear_dispatch(q['TaskId']); continue
|
|
129
|
+
store.clear_dispatch(q['TaskId'])
|
|
130
|
+
try:
|
|
131
|
+
term.start_on_task(store, q['TaskId'], q.get('Agent') or 'coder', actor='router')
|
|
132
|
+
store.add_comment(q['TaskId'], 'router', 'agent', 'Started from the dispatch queue - '
|
|
133
|
+
+ (f'{task_ref(b)} finished with the files it was holding.' if b else 'a session slot freed up.'))
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.warning(f'queued dispatch failed for task {q["TaskId"]}: {e}')
|
|
136
|
+
store.add_comment(q['TaskId'], 'router', 'agent', f'Queued start failed: {str(e)[:200]}')
|
|
137
|
+
finally:
|
|
138
|
+
_DRAINING.release()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def drain_later(store, delay: float = 2.0):
|
|
142
|
+
"""Drain shortly - called from a dying session's own pump thread, which must not host the
|
|
143
|
+
next session's startup (or block on the drain lock)."""
|
|
144
|
+
tm = threading.Timer(delay, lambda: _safe(store))
|
|
145
|
+
tm.daemon = True
|
|
146
|
+
tm.start()
|
|
147
|
+
|
|
148
|
+
def _safe(store):
|
|
149
|
+
try: drain(store)
|
|
150
|
+
except Exception as e: logger.warning(f'dispatch queue drain failed: {e}')
|