taskwarden 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {taskwarden-0.2.0 → taskwarden-0.2.1}/CHANGELOG.md +7 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/PKG-INFO +1 -1
- {taskwarden-0.2.0 → taskwarden-0.2.1}/pyproject.toml +1 -1
- {taskwarden-0.2.0 → taskwarden-0.2.1}/taskwarden/__init__.py +1 -1
- {taskwarden-0.2.0 → taskwarden-0.2.1}/taskwarden/decisions.py +18 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/tests/test_taskwarden.py +30 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/.github/workflows/test.yml +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/.gitignore +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/LICENSE +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/README.md +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/examples/shop.py +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/taskwarden/folders.py +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/taskwarden/gate.py +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/taskwarden/host.py +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/taskwarden/loop.py +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/taskwarden/models.py +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/taskwarden/replies.py +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/taskwarden/sweeper.py +0 -0
- {taskwarden-0.2.0 → taskwarden-0.2.1}/taskwarden/tools.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 (2026-11-04)
|
|
4
|
+
|
|
5
|
+
- An approved call is checked again before it runs, on what will really run: the gate (an
|
|
6
|
+
agent paused or a tool taken away while it waited gets `NOT_ALLOWED`, audited as
|
|
7
|
+
`no_longer_allowed`) and the tool's own `check`, on the owner's edits too (audited as
|
|
8
|
+
`precheck`). Before, only the tool's existence was checked at approval time.
|
|
9
|
+
|
|
3
10
|
## 0.2.0 (2026-11-04)
|
|
4
11
|
|
|
5
12
|
New optional host hooks; a host without them behaves as in 0.1.0.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: taskwarden
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: A durable, owner-approved AI team for a business: agents as folders, tools by risk, owner approvals that survive restarts, on Postgres.
|
|
5
5
|
Project-URL: Repository, https://github.com/dahoai/taskwarden
|
|
6
6
|
Author: DAHO
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "taskwarden"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "A durable, owner-approved AI team for a business: agents as folders, tools by risk, owner approvals that survive restarts, on Postgres."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -11,6 +11,7 @@ from typing import Any, Optional
|
|
|
11
11
|
|
|
12
12
|
from sqlalchemy import select, update
|
|
13
13
|
|
|
14
|
+
from . import gate
|
|
14
15
|
from .loop import MAYBE_SENT, pending_calls, tool_msg
|
|
15
16
|
|
|
16
17
|
# What a parked call reads when the owner wrote in the chat instead of approving it.
|
|
@@ -19,6 +20,8 @@ REVISE_RESULT = ("Not done yet: the owner replied instead of approving: \"{chang
|
|
|
19
20
|
"something else, do that, and ask for this action again only if it still makes sense.")
|
|
20
21
|
# A parked call whose tool no longer exists when the owner decides.
|
|
21
22
|
GONE = "Not run: this action no longer exists. Say what you meant to do another way."
|
|
23
|
+
# A parked call the agent may no longer make (paused, or the tool taken away while it waited).
|
|
24
|
+
NOT_ALLOWED = "Not run: the owner approved it, but you may no longer do this. Say so in your report."
|
|
22
25
|
# What the model reads before the result of a call the owner edited, so its report names
|
|
23
26
|
# what really went out.
|
|
24
27
|
EDITED = "The owner changed {fields} before approving, and it ran as changed. "
|
|
@@ -112,12 +115,27 @@ async def apply(host, db, organization_id: str, task: Any, decision: str, *,
|
|
|
112
115
|
ctx, company = await host.context(db, organization_id)
|
|
113
116
|
region = host.region(company)
|
|
114
117
|
sent = set(task.contacted or [])
|
|
118
|
+
# Checked again now, on what will really run: the call may have waited days, and the
|
|
119
|
+
# owner's edits never went through the check made when it was parked.
|
|
120
|
+
agent = host.for_business(host.agent(task.agent_id), company.get("business_type"))
|
|
121
|
+
state = (await host.agent_states(db, organization_id)).get(agent.id, host.default_state())
|
|
115
122
|
for call, run, changed in zip(calls, runs, changes):
|
|
116
123
|
tool = host.tools.get(call["tool"])
|
|
117
124
|
if tool is None: # renamed or removed while it waited for the owner
|
|
118
125
|
results.append(tool_msg(call.get("tool_call_id", ""), GONE))
|
|
119
126
|
continue
|
|
120
127
|
if run:
|
|
128
|
+
refusal, rule = None, None
|
|
129
|
+
if host.decide(agent, tool, state, self_started=False).outcome == gate.DENY:
|
|
130
|
+
refusal, rule = NOT_ALLOWED, "no_longer_allowed"
|
|
131
|
+
elif tool.check is not None:
|
|
132
|
+
refusal, rule = await host.precheck(tool, ctx, call.get("args") or {}), "precheck"
|
|
133
|
+
if refusal:
|
|
134
|
+
host.audit(db, organization_id=organization_id, agent_id=task.agent_id,
|
|
135
|
+
tool=call["tool"], risk=tool.risk, decision="deny", rule=rule,
|
|
136
|
+
task_id=task.id, actor="owner")
|
|
137
|
+
results.append(tool_msg(call.get("tool_call_id", ""), refusal))
|
|
138
|
+
continue
|
|
121
139
|
key = host.sent_key(call["tool"], call.get("args") or {}, region)
|
|
122
140
|
if key is not None:
|
|
123
141
|
# Saved before it runs, as in the loop: a crash mid-send must not lead to a
|
|
@@ -297,6 +297,36 @@ async def test_an_edited_approval_runs_as_edited_and_the_agent_is_told():
|
|
|
297
297
|
assert json.loads(host.posted[-1]["body"])["calls"][0]["args"] == {"order_id": "A8"}
|
|
298
298
|
|
|
299
299
|
|
|
300
|
+
async def _park_refund(host):
|
|
301
|
+
task, db = _task(), _db()
|
|
302
|
+
assert await loop.run_task(host, db, task, llm=(_llm(Msg(calls=[("refund", {"order_id": "A7"})])), "m")) == "waiting"
|
|
303
|
+
db.execute = AsyncMock(return_value=SimpleNamespace(scalar_one_or_none=lambda: task))
|
|
304
|
+
host.execute = AsyncMock(side_effect=AssertionError("must not run"))
|
|
305
|
+
host.audit = MagicMock()
|
|
306
|
+
return task, db
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
async def test_an_approval_is_checked_again_on_the_args_the_owner_edited():
|
|
310
|
+
host = ShopHost()
|
|
311
|
+
host.tools["refund"] = replace(host.tools["refund"], check=AsyncMock())
|
|
312
|
+
host.precheck = AsyncMock(side_effect=lambda tool, ctx, args:
|
|
313
|
+
"Order A8 was already refunded." if args["order_id"] == "A8" else None)
|
|
314
|
+
task, db = await _park_refund(host)
|
|
315
|
+
await decisions.apply(host, db, "shop1", task, "approve", edited_args={"c0": {"order_id": "A8"}})
|
|
316
|
+
assert {"role": "tool", "tool_call_id": "c0", "content": "Order A8 was already refunded."} in task.state
|
|
317
|
+
assert host.audit.call_args.kwargs["rule"] == "precheck"
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
async def test_an_approval_for_an_agent_paused_while_it_waited_does_not_run():
|
|
321
|
+
host = ShopHost()
|
|
322
|
+
task, db = await _park_refund(host)
|
|
323
|
+
host.agent_states = AsyncMock(return_value={"lead": gate.AgentState(paused=True)})
|
|
324
|
+
await decisions.apply(host, db, "shop1", task, "approve")
|
|
325
|
+
assert {"role": "tool", "tool_call_id": "c0", "content": decisions.NOT_ALLOWED} in task.state
|
|
326
|
+
assert host.audit.call_args.kwargs["rule"] == "no_longer_allowed"
|
|
327
|
+
assert task.status == "queued" # the agent reads why and reports
|
|
328
|
+
|
|
329
|
+
|
|
300
330
|
# ── A long run sheds its old tool results before it is made to wrap up ──
|
|
301
331
|
|
|
302
332
|
def test_old_results_are_cut_to_their_opening_and_the_latest_stay_whole():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|