agentdir-cli 0.7.5__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.
- agentdir/__init__.py +3 -0
- agentdir/__main__.py +6 -0
- agentdir/actors.py +50 -0
- agentdir/artifacts.py +56 -0
- agentdir/audit.py +339 -0
- agentdir/capture.py +187 -0
- agentdir/cli.py +1900 -0
- agentdir/context.py +654 -0
- agentdir/control.py +729 -0
- agentdir/daemon.py +253 -0
- agentdir/doctor.py +115 -0
- agentdir/envelope.py +147 -0
- agentdir/events.py +72 -0
- agentdir/federation.py +530 -0
- agentdir/git.py +45 -0
- agentdir/gitignore.py +106 -0
- agentdir/hooks.py +215 -0
- agentdir/index.py +362 -0
- agentdir/mailbox.py +84 -0
- agentdir/memory.py +1274 -0
- agentdir/query.py +65 -0
- agentdir/redaction.py +42 -0
- agentdir/rendering.py +89 -0
- agentdir/replay.py +31 -0
- agentdir/retention.py +319 -0
- agentdir/review.py +288 -0
- agentdir/secrets.py +158 -0
- agentdir/sessions.py +171 -0
- agentdir/skills.py +685 -0
- agentdir/store.py +270 -0
- agentdir/upgrade.py +252 -0
- agentdir_cli-0.7.5.dist-info/METADATA +393 -0
- agentdir_cli-0.7.5.dist-info/RECORD +37 -0
- agentdir_cli-0.7.5.dist-info/WHEEL +5 -0
- agentdir_cli-0.7.5.dist-info/entry_points.txt +2 -0
- agentdir_cli-0.7.5.dist-info/licenses/LICENSE +21 -0
- agentdir_cli-0.7.5.dist-info/top_level.txt +1 -0
agentdir/control.py
ADDED
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import asdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .audit import audit_claims, audit_session, claims_audit_gaps, session_audit_gaps
|
|
8
|
+
from .context import (
|
|
9
|
+
audit_context_pack,
|
|
10
|
+
build_context_pack,
|
|
11
|
+
emit_context_pack,
|
|
12
|
+
)
|
|
13
|
+
from .daemon import memory_daemon_status
|
|
14
|
+
from .doctor import run_doctor
|
|
15
|
+
from .events import emit_event
|
|
16
|
+
from .federation import doctor_registered_roots, list_registered_roots, list_root_groups
|
|
17
|
+
from .git import git_branch, git_head, git_status_short, workspace_name
|
|
18
|
+
from .index import rebuild_index
|
|
19
|
+
from .memory import DEFAULT_MIN_SCORE, RETRIEVAL_HYBRID, memory_stats
|
|
20
|
+
from .query import query_messages
|
|
21
|
+
from .review import evidence_brief, evidence_rows, format_evidence, format_summary, summarize_session
|
|
22
|
+
from .rendering import rich_status
|
|
23
|
+
from .sessions import SessionState, end_session, ensure_session, read_current_session, require_current_session
|
|
24
|
+
from .store import AgentDirError, init_root, paths_for, require_root
|
|
25
|
+
|
|
26
|
+
EVENT_WORK_STARTED = "work.started"
|
|
27
|
+
EVENT_WORK_FINISHED = "work.finished"
|
|
28
|
+
EVENT_WORK_REPORT_FINAL = "work.report.final"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def adopt_repo(
|
|
32
|
+
root: str | Path,
|
|
33
|
+
*,
|
|
34
|
+
install_hooks_result: list[dict[str, Any]],
|
|
35
|
+
codex_skill_path: str | None,
|
|
36
|
+
generic_guidance_path: str | None = None,
|
|
37
|
+
integrations: list[dict[str, Any]] | None = None,
|
|
38
|
+
gitignore: dict[str, Any] | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
paths = init_root(root)
|
|
41
|
+
doctor = run_doctor(paths.root)
|
|
42
|
+
return {
|
|
43
|
+
"root": str(paths.root),
|
|
44
|
+
"version": _store_version(paths.root),
|
|
45
|
+
"hooks": install_hooks_result,
|
|
46
|
+
"codex_skill": codex_skill_path,
|
|
47
|
+
"generic_guidance": generic_guidance_path,
|
|
48
|
+
"integrations": integrations or [],
|
|
49
|
+
"gitignore": gitignore,
|
|
50
|
+
"doctor": doctor.as_dict(),
|
|
51
|
+
"next": "agentdir work start \"<task>\"",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build_status(
|
|
56
|
+
root: str | Path,
|
|
57
|
+
*,
|
|
58
|
+
scope: str | None = None,
|
|
59
|
+
rebuild: bool = True,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
paths = paths_for(root)
|
|
62
|
+
initialized = (paths.meta / "VERSION").is_file()
|
|
63
|
+
status: dict[str, Any] = {
|
|
64
|
+
"root": {
|
|
65
|
+
"path": str(paths.root),
|
|
66
|
+
"scope": scope or "default",
|
|
67
|
+
"initialized": initialized,
|
|
68
|
+
"version": _store_version(paths.root) if initialized else None,
|
|
69
|
+
},
|
|
70
|
+
"git": {
|
|
71
|
+
"workspace": workspace_name(),
|
|
72
|
+
"branch": git_branch(),
|
|
73
|
+
"head": git_head(),
|
|
74
|
+
"dirty": bool(git_status_short()),
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
if not initialized:
|
|
78
|
+
doctor = run_doctor(paths.root)
|
|
79
|
+
status.update(
|
|
80
|
+
{
|
|
81
|
+
"session": {"active": False, "current": None},
|
|
82
|
+
"context": {"latest_pack": None},
|
|
83
|
+
"evidence": {"count": 0},
|
|
84
|
+
"memory": {"indexed": False},
|
|
85
|
+
"federation": {"roots": [], "groups": []},
|
|
86
|
+
"health": doctor.as_dict(),
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
return status
|
|
90
|
+
|
|
91
|
+
if rebuild:
|
|
92
|
+
rebuild_index(paths.root)
|
|
93
|
+
|
|
94
|
+
current = read_current_session(paths.root)
|
|
95
|
+
summary: dict[str, Any] | None = None
|
|
96
|
+
evidence_count = 0
|
|
97
|
+
if current:
|
|
98
|
+
summary = summarize_session(paths.root, current.session_id, rebuild=False)
|
|
99
|
+
evidence_count = len(evidence_rows(paths.root, current.session_id, rebuild=False))
|
|
100
|
+
|
|
101
|
+
latest_pack = latest_context_pack(
|
|
102
|
+
paths.root,
|
|
103
|
+
current.session_id if current else None,
|
|
104
|
+
fallback_any=True,
|
|
105
|
+
rebuild=False,
|
|
106
|
+
)
|
|
107
|
+
memory = _safe_memory_stats(paths.root)
|
|
108
|
+
memory["daemon"] = memory_daemon_status(paths.root)
|
|
109
|
+
roots = doctor_registered_roots(paths.root)
|
|
110
|
+
groups = list_root_groups(paths.root)
|
|
111
|
+
doctor = run_doctor(paths.root)
|
|
112
|
+
status.update(
|
|
113
|
+
{
|
|
114
|
+
"session": {
|
|
115
|
+
"active": current is not None and current.status == "active",
|
|
116
|
+
"current": asdict(current) if current else None,
|
|
117
|
+
"summary": summary,
|
|
118
|
+
},
|
|
119
|
+
"context": {"latest_pack": latest_pack},
|
|
120
|
+
"evidence": {"count": evidence_count},
|
|
121
|
+
"memory": memory,
|
|
122
|
+
"federation": {
|
|
123
|
+
"roots": roots,
|
|
124
|
+
"groups": groups,
|
|
125
|
+
"registered_roots": len(roots),
|
|
126
|
+
"stale_roots": sum(1 for root in roots if root.get("stale")),
|
|
127
|
+
},
|
|
128
|
+
"health": doctor.as_dict(),
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
return status
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def format_status(status: dict[str, Any]) -> str:
|
|
135
|
+
rendered = rich_status(status)
|
|
136
|
+
if rendered is not None:
|
|
137
|
+
return rendered
|
|
138
|
+
root = status["root"]
|
|
139
|
+
session = status["session"]
|
|
140
|
+
context = status["context"]
|
|
141
|
+
evidence = status["evidence"]
|
|
142
|
+
memory = status["memory"]
|
|
143
|
+
federation = status["federation"]
|
|
144
|
+
health = status["health"]
|
|
145
|
+
lines = [
|
|
146
|
+
"AgentDir Status",
|
|
147
|
+
"",
|
|
148
|
+
f"root={root['path']}",
|
|
149
|
+
f"scope={root['scope']}",
|
|
150
|
+
f"initialized={str(root['initialized']).lower()}",
|
|
151
|
+
f"version={root.get('version') or ''}",
|
|
152
|
+
"",
|
|
153
|
+
f"session_active={str(session['active']).lower()}",
|
|
154
|
+
f"session={session['current']['session_id'] if session.get('current') else ''}",
|
|
155
|
+
f"evidence={evidence['count']}",
|
|
156
|
+
f"context_pack={context['latest_pack']['pack_id'] if context.get('latest_pack') else ''}",
|
|
157
|
+
"",
|
|
158
|
+
f"memory_indexed={str(memory.get('indexed', False)).lower()}",
|
|
159
|
+
f"memory_documents={memory.get('memory_documents', 0)}",
|
|
160
|
+
f"passages={memory.get('passages', 0)}",
|
|
161
|
+
"",
|
|
162
|
+
f"registered_roots={federation['registered_roots']}",
|
|
163
|
+
f"stale_roots={federation['stale_roots']}",
|
|
164
|
+
"",
|
|
165
|
+
f"doctor_ok={str(health['ok']).lower()}",
|
|
166
|
+
]
|
|
167
|
+
for warning in health.get("warnings", []):
|
|
168
|
+
lines.append(f"warning: {warning}")
|
|
169
|
+
for error in health.get("errors", []):
|
|
170
|
+
lines.append(f"error: {error}")
|
|
171
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def start_work(
|
|
175
|
+
root: str | Path,
|
|
176
|
+
task: str,
|
|
177
|
+
*,
|
|
178
|
+
actor: str = "agent",
|
|
179
|
+
emit_context: bool = False,
|
|
180
|
+
federated: bool = False,
|
|
181
|
+
federation_group: str | None = None,
|
|
182
|
+
memory_limit: int = 8,
|
|
183
|
+
evidence_limit: int = 20,
|
|
184
|
+
recent_limit: int = 5,
|
|
185
|
+
min_score: float = DEFAULT_MIN_SCORE,
|
|
186
|
+
retrieval_mode: str = RETRIEVAL_HYBRID,
|
|
187
|
+
) -> dict[str, Any]:
|
|
188
|
+
paths = init_root(root)
|
|
189
|
+
session = ensure_session(paths.root, title=task, actor=actor)
|
|
190
|
+
emit_event(
|
|
191
|
+
paths.root,
|
|
192
|
+
session_id=session.session_id,
|
|
193
|
+
event_type=EVENT_WORK_STARTED,
|
|
194
|
+
subject=f"work started: {task}",
|
|
195
|
+
body=f"task={task}\nsession_id={session.session_id}",
|
|
196
|
+
from_actor=actor,
|
|
197
|
+
workspace=session.workspace,
|
|
198
|
+
git_head=session.git_head,
|
|
199
|
+
)
|
|
200
|
+
pack = build_context_pack(
|
|
201
|
+
paths.root,
|
|
202
|
+
task,
|
|
203
|
+
session_id=session.session_id,
|
|
204
|
+
memory_limit=memory_limit,
|
|
205
|
+
evidence_limit=evidence_limit,
|
|
206
|
+
recent_limit=recent_limit,
|
|
207
|
+
min_score=min_score,
|
|
208
|
+
federated=federated or bool(federation_group),
|
|
209
|
+
federation_group=federation_group,
|
|
210
|
+
retrieval_mode=retrieval_mode,
|
|
211
|
+
exclude_session_from_memory=True,
|
|
212
|
+
)
|
|
213
|
+
emitted = None
|
|
214
|
+
if emit_context:
|
|
215
|
+
emitted = emit_context_pack(
|
|
216
|
+
paths.root,
|
|
217
|
+
pack,
|
|
218
|
+
selection_policy={
|
|
219
|
+
"memory_limit": memory_limit,
|
|
220
|
+
"evidence_limit": evidence_limit,
|
|
221
|
+
"recent_limit": recent_limit,
|
|
222
|
+
"min_score": min_score,
|
|
223
|
+
"federated": federated or bool(federation_group),
|
|
224
|
+
"federation_group": federation_group,
|
|
225
|
+
"retrieval_mode": retrieval_mode,
|
|
226
|
+
},
|
|
227
|
+
actor=actor,
|
|
228
|
+
scope=federation_group or ("federated" if federated else "project"),
|
|
229
|
+
)
|
|
230
|
+
return {
|
|
231
|
+
"session": asdict(session),
|
|
232
|
+
"task": task,
|
|
233
|
+
"context": {
|
|
234
|
+
"memory_hits": len(pack.get("memory_hits") or []),
|
|
235
|
+
"evidence": len(pack.get("evidence") or []),
|
|
236
|
+
"recent_session_summaries": len(pack.get("recent_session_summaries") or []),
|
|
237
|
+
"federated": bool(pack.get("federated")),
|
|
238
|
+
"federation_group": federation_group,
|
|
239
|
+
},
|
|
240
|
+
"context_pack": emitted.manifest if emitted else None,
|
|
241
|
+
"event_path": str(emitted.event_path) if emitted else None,
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def format_work_start(result: dict[str, Any]) -> str:
|
|
246
|
+
session = result["session"]
|
|
247
|
+
context = result["context"]
|
|
248
|
+
lines = [
|
|
249
|
+
f"session={session['session_id']}",
|
|
250
|
+
f"task={result['task']}",
|
|
251
|
+
f"memory_hits={context['memory_hits']}",
|
|
252
|
+
f"evidence={context['evidence']}",
|
|
253
|
+
f"recent_session_summaries={context['recent_session_summaries']}",
|
|
254
|
+
f"federated={str(context['federated']).lower()}",
|
|
255
|
+
]
|
|
256
|
+
if context.get("federation_group"):
|
|
257
|
+
lines.append(f"group={context['federation_group']}")
|
|
258
|
+
if result.get("context_pack"):
|
|
259
|
+
lines.append(f"context_pack={result['context_pack']['pack_id']}")
|
|
260
|
+
return "\n".join(lines) + "\n"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def finish_work(
|
|
264
|
+
root: str | Path,
|
|
265
|
+
*,
|
|
266
|
+
session_id: str | None = None,
|
|
267
|
+
actor: str = "agent",
|
|
268
|
+
run_health_check: bool = True,
|
|
269
|
+
end: bool = True,
|
|
270
|
+
claims_text: str | None = None,
|
|
271
|
+
) -> dict[str, Any]:
|
|
272
|
+
paths = require_root(root)
|
|
273
|
+
session = _resolve_session(paths.root, session_id)
|
|
274
|
+
report = build_final_report(
|
|
275
|
+
paths.root,
|
|
276
|
+
session_id=session.session_id,
|
|
277
|
+
run_health_check=run_health_check,
|
|
278
|
+
claims_text=claims_text,
|
|
279
|
+
finishing=True,
|
|
280
|
+
)
|
|
281
|
+
rendered = format_final_report(report)
|
|
282
|
+
event = emit_event(
|
|
283
|
+
paths.root,
|
|
284
|
+
session_id=session.session_id,
|
|
285
|
+
event_type=EVENT_WORK_REPORT_FINAL,
|
|
286
|
+
subject="final report",
|
|
287
|
+
body=rendered,
|
|
288
|
+
from_actor=actor,
|
|
289
|
+
workspace=session.workspace,
|
|
290
|
+
git_head=git_head(),
|
|
291
|
+
)
|
|
292
|
+
emit_event(
|
|
293
|
+
paths.root,
|
|
294
|
+
session_id=session.session_id,
|
|
295
|
+
event_type=EVENT_WORK_FINISHED,
|
|
296
|
+
subject=f"work finished: {session.title}",
|
|
297
|
+
body=f"session_id={session.session_id}\nreport_event={event.path}",
|
|
298
|
+
from_actor=actor,
|
|
299
|
+
workspace=session.workspace,
|
|
300
|
+
git_head=git_head(),
|
|
301
|
+
)
|
|
302
|
+
ended = None
|
|
303
|
+
if end:
|
|
304
|
+
ended = end_session(
|
|
305
|
+
paths.root,
|
|
306
|
+
summary="AgentDir work finished; final report emitted.",
|
|
307
|
+
actor=actor,
|
|
308
|
+
)
|
|
309
|
+
return {
|
|
310
|
+
"report": report,
|
|
311
|
+
"rendered": rendered,
|
|
312
|
+
"event_path": str(event.path),
|
|
313
|
+
"ended_session": asdict(ended) if ended else None,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def build_final_report(
|
|
318
|
+
root: str | Path,
|
|
319
|
+
*,
|
|
320
|
+
session_id: str | None = None,
|
|
321
|
+
run_health_check: bool = True,
|
|
322
|
+
claims_text: str | None = None,
|
|
323
|
+
finishing: bool = False,
|
|
324
|
+
) -> dict[str, Any]:
|
|
325
|
+
paths = require_root(root)
|
|
326
|
+
session = _resolve_session(paths.root, session_id)
|
|
327
|
+
rebuild_index(paths.root)
|
|
328
|
+
summary = summarize_session(paths.root, session.session_id, rebuild=False)
|
|
329
|
+
evidence = evidence_rows(paths.root, session.session_id, rebuild=False)
|
|
330
|
+
latest_pack = latest_context_pack(paths.root, session.session_id, rebuild=False)
|
|
331
|
+
context_audit = None
|
|
332
|
+
if latest_pack:
|
|
333
|
+
try:
|
|
334
|
+
context_audit = audit_context_pack(paths.root, latest_pack["pack_id"], rebuild=False)
|
|
335
|
+
except AgentDirError as exc:
|
|
336
|
+
context_audit = {"error": str(exc), "pack_id": latest_pack["pack_id"]}
|
|
337
|
+
doctor = run_doctor(paths.root).as_dict() if run_health_check else None
|
|
338
|
+
session_audit = audit_session(
|
|
339
|
+
paths.root,
|
|
340
|
+
session.session_id,
|
|
341
|
+
finishing=finishing,
|
|
342
|
+
run_health_check=run_health_check,
|
|
343
|
+
rebuild=False,
|
|
344
|
+
summary=summary,
|
|
345
|
+
evidence=evidence,
|
|
346
|
+
latest_pack=latest_pack,
|
|
347
|
+
context_audit=context_audit,
|
|
348
|
+
doctor=doctor,
|
|
349
|
+
)
|
|
350
|
+
claims_audit = (
|
|
351
|
+
audit_claims(
|
|
352
|
+
paths.root,
|
|
353
|
+
claims_text,
|
|
354
|
+
session.session_id,
|
|
355
|
+
rebuild=False,
|
|
356
|
+
summary=summary,
|
|
357
|
+
evidence=evidence,
|
|
358
|
+
)
|
|
359
|
+
if claims_text is not None
|
|
360
|
+
else None
|
|
361
|
+
)
|
|
362
|
+
gaps = _known_gaps(summary, evidence, context_audit, doctor, session_audit, claims_audit)
|
|
363
|
+
brief = evidence_brief(evidence)
|
|
364
|
+
handoff = _agent_handoff(
|
|
365
|
+
summary=summary,
|
|
366
|
+
evidence_brief_payload=brief,
|
|
367
|
+
context_audit=context_audit,
|
|
368
|
+
doctor=doctor,
|
|
369
|
+
session_audit=session_audit,
|
|
370
|
+
claims_audit=claims_audit,
|
|
371
|
+
known_gaps=gaps,
|
|
372
|
+
)
|
|
373
|
+
return {
|
|
374
|
+
"task": session.title,
|
|
375
|
+
"session": asdict(session),
|
|
376
|
+
"summary": summary,
|
|
377
|
+
"evidence": evidence,
|
|
378
|
+
"evidence_brief": brief,
|
|
379
|
+
"context": {
|
|
380
|
+
"latest_pack": latest_pack,
|
|
381
|
+
"audit": context_audit,
|
|
382
|
+
},
|
|
383
|
+
"session_audit": session_audit,
|
|
384
|
+
"claim_support": claims_audit,
|
|
385
|
+
"agent_handoff": handoff,
|
|
386
|
+
"git": {
|
|
387
|
+
"workspace": session.workspace,
|
|
388
|
+
"branch": git_branch(),
|
|
389
|
+
"head": git_head(),
|
|
390
|
+
"dirty": bool(git_status_short()),
|
|
391
|
+
},
|
|
392
|
+
"health": doctor,
|
|
393
|
+
"known_gaps": gaps,
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def format_final_report(report: dict[str, Any]) -> str:
|
|
398
|
+
summary = report["summary"]
|
|
399
|
+
context = report["context"]
|
|
400
|
+
health = report.get("health")
|
|
401
|
+
session_audit = report.get("session_audit") or {}
|
|
402
|
+
claim_support = report.get("claim_support")
|
|
403
|
+
handoff = report.get("agent_handoff") or {}
|
|
404
|
+
lines = [
|
|
405
|
+
"# AgentDir Final Report",
|
|
406
|
+
"",
|
|
407
|
+
"## Task",
|
|
408
|
+
"",
|
|
409
|
+
report["task"],
|
|
410
|
+
"",
|
|
411
|
+
"## Session",
|
|
412
|
+
"",
|
|
413
|
+
f"- session: `{summary['session_id']}`",
|
|
414
|
+
f"- events_before_final_report: {summary['events']}",
|
|
415
|
+
f"- tool_results: {summary['tool_results']}",
|
|
416
|
+
f"- failed_tools: {summary['failed_tools']}",
|
|
417
|
+
"",
|
|
418
|
+
"## Evidence",
|
|
419
|
+
"",
|
|
420
|
+
]
|
|
421
|
+
if report["evidence"]:
|
|
422
|
+
lines.append("```text")
|
|
423
|
+
lines.append(format_evidence(report["evidence"]))
|
|
424
|
+
lines.append("```")
|
|
425
|
+
else:
|
|
426
|
+
lines.append("No evidence captured.")
|
|
427
|
+
lines.extend(["", "## Context", ""])
|
|
428
|
+
latest_pack = context.get("latest_pack")
|
|
429
|
+
audit = context.get("audit")
|
|
430
|
+
if latest_pack:
|
|
431
|
+
lines.append(f"- pack: `{latest_pack['pack_id']}`")
|
|
432
|
+
else:
|
|
433
|
+
lines.append("- pack: none")
|
|
434
|
+
if audit and not audit.get("error"):
|
|
435
|
+
lines.append(f"- retrieved: {audit['retrieved_count']}")
|
|
436
|
+
lines.append(f"- consumed: {audit['consumed_count']}")
|
|
437
|
+
lines.append(f"- cited: {audit['cited_count']}")
|
|
438
|
+
lines.append(f"- evidence_backed: {audit['evidence_backed_count']}")
|
|
439
|
+
elif audit and audit.get("error"):
|
|
440
|
+
lines.append(f"- audit_error: {audit['error']}")
|
|
441
|
+
lines.extend(["", "## Session Audit", ""])
|
|
442
|
+
if session_audit:
|
|
443
|
+
lines.append(f"- ok: {str(session_audit.get('ok', False)).lower()}")
|
|
444
|
+
for check in session_audit.get("checks") or []:
|
|
445
|
+
lines.append(f"- {check['id']}: {check['status']} - {check['message']}")
|
|
446
|
+
else:
|
|
447
|
+
lines.append("- audit: not run")
|
|
448
|
+
if claim_support is not None:
|
|
449
|
+
lines.extend(["", "## Claim Support", ""])
|
|
450
|
+
if claim_support.get("claims"):
|
|
451
|
+
for claim in claim_support["claims"]:
|
|
452
|
+
lines.append(f"- {claim['family']}: {claim['status']} - {claim['message']}")
|
|
453
|
+
else:
|
|
454
|
+
lines.append("- claims: none detected")
|
|
455
|
+
lines.extend(["", "## Agent Handoff", ""])
|
|
456
|
+
if handoff:
|
|
457
|
+
lines.append(f"- status: {handoff.get('status')}")
|
|
458
|
+
actions = handoff.get("recommended_agent_actions") or []
|
|
459
|
+
if actions:
|
|
460
|
+
for action in actions:
|
|
461
|
+
lines.append(f"- action: {action}")
|
|
462
|
+
else:
|
|
463
|
+
lines.append("- action: none")
|
|
464
|
+
else:
|
|
465
|
+
lines.append("- handoff: not available")
|
|
466
|
+
lines.extend(["", "## Health", ""])
|
|
467
|
+
if health:
|
|
468
|
+
lines.append(f"- doctor_ok: {str(health['ok']).lower()}")
|
|
469
|
+
for warning in health.get("warnings", []):
|
|
470
|
+
lines.append(f"- warning: {warning}")
|
|
471
|
+
for error in health.get("errors", []):
|
|
472
|
+
lines.append(f"- error: {error}")
|
|
473
|
+
else:
|
|
474
|
+
lines.append("- doctor: not run")
|
|
475
|
+
lines.extend(["", "## Known Gaps", ""])
|
|
476
|
+
if report["known_gaps"]:
|
|
477
|
+
for gap in report["known_gaps"]:
|
|
478
|
+
lines.append(f"- {gap}")
|
|
479
|
+
else:
|
|
480
|
+
lines.append("- none")
|
|
481
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _agent_handoff(
|
|
485
|
+
*,
|
|
486
|
+
summary: dict[str, Any],
|
|
487
|
+
evidence_brief_payload: dict[str, Any],
|
|
488
|
+
context_audit: dict[str, Any] | None,
|
|
489
|
+
doctor: dict[str, Any] | None,
|
|
490
|
+
session_audit: dict[str, Any] | None,
|
|
491
|
+
claims_audit: dict[str, Any] | None,
|
|
492
|
+
known_gaps: list[str],
|
|
493
|
+
) -> dict[str, Any]:
|
|
494
|
+
failed_evidence = evidence_brief_payload.get("failed_evidence") or []
|
|
495
|
+
fail_checks = [
|
|
496
|
+
check
|
|
497
|
+
for check in (session_audit or {}).get("checks", [])
|
|
498
|
+
if check.get("status") == "fail"
|
|
499
|
+
]
|
|
500
|
+
unsupported_claims = []
|
|
501
|
+
contradicted_claims = []
|
|
502
|
+
if claims_audit:
|
|
503
|
+
unsupported_claims = [
|
|
504
|
+
claim
|
|
505
|
+
for claim in claims_audit.get("claims", [])
|
|
506
|
+
if claim.get("status") == "unsupported"
|
|
507
|
+
]
|
|
508
|
+
contradicted_claims = [
|
|
509
|
+
claim
|
|
510
|
+
for claim in claims_audit.get("claims", [])
|
|
511
|
+
if claim.get("status") == "contradicted"
|
|
512
|
+
]
|
|
513
|
+
doctor_errors = (doctor or {}).get("errors") or []
|
|
514
|
+
needs_attention = bool(failed_evidence or fail_checks or unsupported_claims or contradicted_claims or doctor_errors)
|
|
515
|
+
return {
|
|
516
|
+
"status": "needs_attention" if needs_attention else "ok",
|
|
517
|
+
"verification": evidence_brief_payload.get("families") or [],
|
|
518
|
+
"failed_evidence": failed_evidence,
|
|
519
|
+
"claim_support": claims_audit,
|
|
520
|
+
"context_lineage": _context_lineage(context_audit),
|
|
521
|
+
"known_gaps": known_gaps,
|
|
522
|
+
"recommended_agent_actions": _recommended_agent_actions(
|
|
523
|
+
summary=summary,
|
|
524
|
+
failed_evidence=failed_evidence,
|
|
525
|
+
unsupported_claims=unsupported_claims,
|
|
526
|
+
contradicted_claims=contradicted_claims,
|
|
527
|
+
doctor_errors=doctor_errors,
|
|
528
|
+
session_audit=session_audit,
|
|
529
|
+
),
|
|
530
|
+
"final_response_guidance": _final_response_guidance(
|
|
531
|
+
failed_evidence=failed_evidence,
|
|
532
|
+
unsupported_claims=unsupported_claims,
|
|
533
|
+
contradicted_claims=contradicted_claims,
|
|
534
|
+
doctor_errors=doctor_errors,
|
|
535
|
+
),
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
def _context_lineage(context_audit: dict[str, Any] | None) -> dict[str, Any]:
|
|
540
|
+
if context_audit is None:
|
|
541
|
+
return {
|
|
542
|
+
"pack_id": None,
|
|
543
|
+
"retrieved": 0,
|
|
544
|
+
"consumed": 0,
|
|
545
|
+
"cited": 0,
|
|
546
|
+
"evidence_backed": 0,
|
|
547
|
+
"ok": False,
|
|
548
|
+
}
|
|
549
|
+
if context_audit.get("error"):
|
|
550
|
+
return {
|
|
551
|
+
"pack_id": context_audit.get("pack_id"),
|
|
552
|
+
"retrieved": 0,
|
|
553
|
+
"consumed": 0,
|
|
554
|
+
"cited": 0,
|
|
555
|
+
"evidence_backed": 0,
|
|
556
|
+
"ok": False,
|
|
557
|
+
"error": context_audit.get("error"),
|
|
558
|
+
}
|
|
559
|
+
retrieved = context_audit.get("retrieved_count", 0)
|
|
560
|
+
consumed = context_audit.get("consumed_count", 0)
|
|
561
|
+
cited = context_audit.get("cited_count", 0)
|
|
562
|
+
return {
|
|
563
|
+
"pack_id": context_audit.get("pack_id"),
|
|
564
|
+
"retrieved": retrieved,
|
|
565
|
+
"consumed": consumed,
|
|
566
|
+
"cited": cited,
|
|
567
|
+
"evidence_backed": context_audit.get("evidence_backed_count", 0),
|
|
568
|
+
"ok": not ((retrieved and not consumed) or (consumed and not cited)),
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _recommended_agent_actions(
|
|
573
|
+
*,
|
|
574
|
+
summary: dict[str, Any],
|
|
575
|
+
failed_evidence: list[dict[str, Any]],
|
|
576
|
+
unsupported_claims: list[dict[str, Any]],
|
|
577
|
+
contradicted_claims: list[dict[str, Any]],
|
|
578
|
+
doctor_errors: list[str],
|
|
579
|
+
session_audit: dict[str, Any] | None,
|
|
580
|
+
) -> list[str]:
|
|
581
|
+
actions: list[str] = []
|
|
582
|
+
if failed_evidence:
|
|
583
|
+
actions.append("Inspect failed evidence and rerun the relevant check after fixing it.")
|
|
584
|
+
if contradicted_claims:
|
|
585
|
+
actions.append("Do not claim contradicted checks passed until newer successful evidence exists.")
|
|
586
|
+
if unsupported_claims:
|
|
587
|
+
families = ", ".join(sorted({str(claim.get("family")) for claim in unsupported_claims}))
|
|
588
|
+
actions.append(f"Capture evidence or remove unsupported final-response claims for: {families}.")
|
|
589
|
+
if doctor_errors:
|
|
590
|
+
actions.append("Run `agentdir doctor` and resolve health errors before treating the session as clean.")
|
|
591
|
+
if summary.get("tool_results", 0) == 0:
|
|
592
|
+
actions.append("Run evidence-bearing verification through `agentdir run -- <command>` when final claims need support.")
|
|
593
|
+
if session_audit:
|
|
594
|
+
failing_ids = [
|
|
595
|
+
check.get("id")
|
|
596
|
+
for check in session_audit.get("checks", [])
|
|
597
|
+
if check.get("status") == "fail" and check.get("id") != "failed_tool_results"
|
|
598
|
+
]
|
|
599
|
+
for check_id in failing_ids:
|
|
600
|
+
actions.append(f"Resolve failing session audit check: {check_id}.")
|
|
601
|
+
return actions
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _final_response_guidance(
|
|
605
|
+
*,
|
|
606
|
+
failed_evidence: list[dict[str, Any]],
|
|
607
|
+
unsupported_claims: list[dict[str, Any]],
|
|
608
|
+
contradicted_claims: list[dict[str, Any]],
|
|
609
|
+
doctor_errors: list[str],
|
|
610
|
+
) -> list[str]:
|
|
611
|
+
guidance = [
|
|
612
|
+
"Base final verification claims only on supported AgentDir evidence.",
|
|
613
|
+
"Mention AgentDir only when it clarifies evidence, blockers, or setup problems.",
|
|
614
|
+
]
|
|
615
|
+
if failed_evidence:
|
|
616
|
+
guidance.append("Call out failed checks plainly instead of summarizing the session as clean.")
|
|
617
|
+
if unsupported_claims:
|
|
618
|
+
guidance.append("Avoid unsupported test, lint, typecheck, build, doctor, or release claims.")
|
|
619
|
+
if contradicted_claims:
|
|
620
|
+
guidance.append("Treat contradicted claims as blockers until newer passing evidence exists.")
|
|
621
|
+
if doctor_errors:
|
|
622
|
+
guidance.append("Surface doctor errors as setup or health blockers.")
|
|
623
|
+
return guidance
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def latest_context_pack(
|
|
627
|
+
root: str | Path,
|
|
628
|
+
session_id: str | None = None,
|
|
629
|
+
*,
|
|
630
|
+
fallback_any: bool = False,
|
|
631
|
+
rebuild: bool = True,
|
|
632
|
+
) -> dict[str, Any] | None:
|
|
633
|
+
if rebuild:
|
|
634
|
+
rebuild_index(root)
|
|
635
|
+
rows = query_messages(
|
|
636
|
+
root,
|
|
637
|
+
session_id=session_id,
|
|
638
|
+
event_type="context.pack.created",
|
|
639
|
+
limit=10_000,
|
|
640
|
+
)
|
|
641
|
+
if not rows and session_id and fallback_any:
|
|
642
|
+
rows = query_messages(root, event_type="context.pack.created", limit=10_000)
|
|
643
|
+
for row in reversed(rows):
|
|
644
|
+
pack_id = _pack_id_from_body(row.get("body_text") or "")
|
|
645
|
+
if pack_id:
|
|
646
|
+
return {
|
|
647
|
+
"pack_id": pack_id,
|
|
648
|
+
"session_id": row.get("session_id"),
|
|
649
|
+
"event_path": row.get("file_path"),
|
|
650
|
+
"subject": row.get("subject"),
|
|
651
|
+
"date_utc": row.get("date_utc"),
|
|
652
|
+
}
|
|
653
|
+
return None
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _resolve_session(root: str | Path, session_id: str | None) -> SessionState:
|
|
657
|
+
if session_id:
|
|
658
|
+
current = read_current_session(root)
|
|
659
|
+
if current and current.session_id == session_id:
|
|
660
|
+
return current
|
|
661
|
+
summary = summarize_session(root, session_id)
|
|
662
|
+
return SessionState(
|
|
663
|
+
session_id=session_id,
|
|
664
|
+
title=session_id,
|
|
665
|
+
actor="agent",
|
|
666
|
+
workspace=workspace_name(),
|
|
667
|
+
git_head=git_head(),
|
|
668
|
+
started_at=summary.get("first_event") or "",
|
|
669
|
+
status="unknown",
|
|
670
|
+
)
|
|
671
|
+
return require_current_session(root)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _safe_memory_stats(root: str | Path) -> dict[str, Any]:
|
|
675
|
+
try:
|
|
676
|
+
stats = memory_stats(root)
|
|
677
|
+
except Exception as exc:
|
|
678
|
+
return {"indexed": False, "error": str(exc)}
|
|
679
|
+
return {"indexed": True, **stats}
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _store_version(root: str | Path) -> str | None:
|
|
683
|
+
path = paths_for(root).meta / "VERSION"
|
|
684
|
+
if not path.is_file():
|
|
685
|
+
return None
|
|
686
|
+
return path.read_text(encoding="utf-8").strip()
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def _pack_id_from_body(body: str) -> str | None:
|
|
690
|
+
for line in body.splitlines():
|
|
691
|
+
if line.startswith("pack_id="):
|
|
692
|
+
return line.split("=", 1)[1].strip() or None
|
|
693
|
+
return None
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _known_gaps(
|
|
697
|
+
summary: dict[str, Any],
|
|
698
|
+
evidence: list[dict[str, Any]],
|
|
699
|
+
context_audit: dict[str, Any] | None,
|
|
700
|
+
doctor: dict[str, Any] | None,
|
|
701
|
+
session_audit: dict[str, Any] | None = None,
|
|
702
|
+
claims_audit: dict[str, Any] | None = None,
|
|
703
|
+
) -> list[str]:
|
|
704
|
+
gaps: list[str] = []
|
|
705
|
+
if session_audit:
|
|
706
|
+
gaps.extend(session_audit_gaps(session_audit))
|
|
707
|
+
if claims_audit:
|
|
708
|
+
gaps.extend(claims_audit_gaps(claims_audit))
|
|
709
|
+
if session_audit:
|
|
710
|
+
return gaps
|
|
711
|
+
if summary.get("failed_tools"):
|
|
712
|
+
gaps.append(f"{summary['failed_tools']} captured tool result(s) failed")
|
|
713
|
+
if not evidence:
|
|
714
|
+
gaps.append("No evidence captured")
|
|
715
|
+
if context_audit is None:
|
|
716
|
+
gaps.append("No context pack recorded")
|
|
717
|
+
elif context_audit.get("error"):
|
|
718
|
+
gaps.append(f"Context audit failed: {context_audit['error']}")
|
|
719
|
+
else:
|
|
720
|
+
if context_audit.get("retrieved_count", 0) and not context_audit.get("consumed_count", 0):
|
|
721
|
+
gaps.append("Context pack emitted but no sources consumed")
|
|
722
|
+
if context_audit.get("consumed_count", 0) and not context_audit.get("cited_count", 0):
|
|
723
|
+
gaps.append("Context sources consumed but not cited")
|
|
724
|
+
if doctor:
|
|
725
|
+
for warning in doctor.get("warnings", []):
|
|
726
|
+
gaps.append(f"Doctor warning: {warning}")
|
|
727
|
+
for error in doctor.get("errors", []):
|
|
728
|
+
gaps.append(f"Doctor error: {error}")
|
|
729
|
+
return gaps
|