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/context.py
ADDED
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import tempfile
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import UTC, datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
from uuid import uuid4
|
|
10
|
+
|
|
11
|
+
from .artifacts import add_artifact, artifact_headers, artifact_path
|
|
12
|
+
from .events import emit_event
|
|
13
|
+
from .federation import search_federated_memory
|
|
14
|
+
from .index import connect_index, rebuild_index
|
|
15
|
+
from .memory import DEFAULT_MIN_SCORE, RETRIEVAL_HYBRID, recent_session_summaries, search_memory
|
|
16
|
+
from .query import query_messages
|
|
17
|
+
from .review import evidence_rows, format_evidence, format_summary, summarize_session
|
|
18
|
+
from .sessions import read_current_session
|
|
19
|
+
from .store import AgentDirError
|
|
20
|
+
|
|
21
|
+
CONTEXT_PROTOCOL = "agentdir.context-pack.v1"
|
|
22
|
+
CONTEXT_ENFORCEMENT_MODE = "advisory"
|
|
23
|
+
EVENT_CONTEXT_PACK_CREATED = "context.pack.created"
|
|
24
|
+
EVENT_CONTEXT_PACK_CONSUMED = "context.pack.consumed"
|
|
25
|
+
EVENT_CONTEXT_SOURCES_CITED = "context.sources.cited"
|
|
26
|
+
HEADER_PROTOCOL = "X-AgentDir-Protocol"
|
|
27
|
+
HEADER_PACK_ID = "X-AgentDir-Pack-Id"
|
|
28
|
+
HEADER_CONTEXT_QUERY = "X-AgentDir-Context-Query"
|
|
29
|
+
HEADER_CONTEXT_SCOPE = "X-AgentDir-Context-Scope"
|
|
30
|
+
HEADER_SOURCE_ID = "X-AgentDir-Source-Id"
|
|
31
|
+
HEADER_CONSUMPTION_PURPOSE = "X-AgentDir-Consumption-Purpose"
|
|
32
|
+
HEADER_ENFORCEMENT_MODE = "X-AgentDir-Enforcement-Mode"
|
|
33
|
+
CONSUMPTION_PURPOSES = ("plan", "tool", "answer", "handoff")
|
|
34
|
+
EVIDENCE_EVENTS = {"tool.call", "tool.result", "file.diff"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class EmittedContextPack:
|
|
39
|
+
manifest: dict[str, Any]
|
|
40
|
+
event_path: Path
|
|
41
|
+
artifact_sha256: str
|
|
42
|
+
artifact_path: Path
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def build_context_pack(
|
|
46
|
+
root: str | Path,
|
|
47
|
+
task: str,
|
|
48
|
+
*,
|
|
49
|
+
session_id: str | None = None,
|
|
50
|
+
memory_limit: int = 8,
|
|
51
|
+
evidence_limit: int = 20,
|
|
52
|
+
recent_limit: int = 5,
|
|
53
|
+
min_score: float = DEFAULT_MIN_SCORE,
|
|
54
|
+
federated: bool = False,
|
|
55
|
+
federation_group: str | None = None,
|
|
56
|
+
retrieval_mode: str = RETRIEVAL_HYBRID,
|
|
57
|
+
exclude_session_from_memory: bool = False,
|
|
58
|
+
) -> dict[str, Any]:
|
|
59
|
+
rebuild_index(root)
|
|
60
|
+
resolved_session = session_id
|
|
61
|
+
if resolved_session is None:
|
|
62
|
+
current = read_current_session(root)
|
|
63
|
+
resolved_session = current.session_id if current else None
|
|
64
|
+
|
|
65
|
+
search_limit = memory_limit if not (resolved_session and exclude_session_from_memory) else max(memory_limit * 3, memory_limit + 5)
|
|
66
|
+
memory_hits = (
|
|
67
|
+
search_federated_memory(
|
|
68
|
+
root,
|
|
69
|
+
task,
|
|
70
|
+
limit=search_limit,
|
|
71
|
+
min_score=min_score,
|
|
72
|
+
retrieval_mode=retrieval_mode,
|
|
73
|
+
group=federation_group,
|
|
74
|
+
)
|
|
75
|
+
if federated or federation_group
|
|
76
|
+
else search_memory(
|
|
77
|
+
root,
|
|
78
|
+
task,
|
|
79
|
+
limit=search_limit,
|
|
80
|
+
min_score=min_score,
|
|
81
|
+
retrieval_mode=retrieval_mode,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
if resolved_session and exclude_session_from_memory:
|
|
85
|
+
memory_hits = [row for row in memory_hits if row.get("session_id") != resolved_session][:memory_limit]
|
|
86
|
+
recent = [
|
|
87
|
+
row
|
|
88
|
+
for row in recent_session_summaries(root, limit=recent_limit + 5)
|
|
89
|
+
if row.get("session_id") != resolved_session
|
|
90
|
+
][:recent_limit]
|
|
91
|
+
else:
|
|
92
|
+
memory_hits = memory_hits[:memory_limit]
|
|
93
|
+
recent = recent_session_summaries(root, limit=recent_limit)
|
|
94
|
+
evidence = evidence_rows(root, resolved_session, rebuild=False)[:evidence_limit] if resolved_session else []
|
|
95
|
+
current_summary = summarize_session(root, resolved_session, rebuild=False) if resolved_session else None
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
"task": task,
|
|
99
|
+
"session_id": resolved_session,
|
|
100
|
+
"current_summary": current_summary,
|
|
101
|
+
"memory_hits": memory_hits,
|
|
102
|
+
"federated": bool(federated or federation_group),
|
|
103
|
+
"federation_group": federation_group,
|
|
104
|
+
"retrieval_mode": retrieval_mode,
|
|
105
|
+
"recent_session_summaries": recent,
|
|
106
|
+
"evidence": evidence,
|
|
107
|
+
"instructions": [
|
|
108
|
+
"Use memory hits as retrieval hints, not proof.",
|
|
109
|
+
"Use evidence rows for claims about commands, hooks, and diffs.",
|
|
110
|
+
"Run fresh verification before reporting completion.",
|
|
111
|
+
],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_context_manifest(
|
|
116
|
+
pack: dict[str, Any],
|
|
117
|
+
*,
|
|
118
|
+
pack_id: str | None = None,
|
|
119
|
+
selection_policy: dict[str, Any] | None = None,
|
|
120
|
+
) -> dict[str, Any]:
|
|
121
|
+
resolved_pack_id = pack_id or new_pack_id()
|
|
122
|
+
memory_sources = [_source_entry(row, origin="memory_hit") for row in pack.get("memory_hits") or []]
|
|
123
|
+
recent_summaries = [
|
|
124
|
+
_source_entry(row, origin="recent_summary")
|
|
125
|
+
for row in pack.get("recent_session_summaries") or []
|
|
126
|
+
]
|
|
127
|
+
evidence = [_source_entry(row, origin="evidence") for row in pack.get("evidence") or []]
|
|
128
|
+
sources = _dedupe_sources([*memory_sources, *recent_summaries, *evidence])
|
|
129
|
+
return {
|
|
130
|
+
"protocol": CONTEXT_PROTOCOL,
|
|
131
|
+
"pack_id": resolved_pack_id,
|
|
132
|
+
"task": pack["task"],
|
|
133
|
+
"session_id": pack.get("session_id"),
|
|
134
|
+
"generated_at": now_iso(),
|
|
135
|
+
"selection_policy": selection_policy or {},
|
|
136
|
+
"federated": bool(pack.get("federated")),
|
|
137
|
+
"federation_group": pack.get("federation_group"),
|
|
138
|
+
"retrieval_mode": pack.get("retrieval_mode") or RETRIEVAL_HYBRID,
|
|
139
|
+
"sources": sources,
|
|
140
|
+
"memory_hits": memory_sources,
|
|
141
|
+
"recent_summaries": recent_summaries,
|
|
142
|
+
"evidence": evidence,
|
|
143
|
+
"instructions": pack.get("instructions") or [],
|
|
144
|
+
"enforcement_boundary": (
|
|
145
|
+
"advisory: AgentDir records that a cooperative agent requested, "
|
|
146
|
+
"consumed, and cited context, but it cannot prove model attention."
|
|
147
|
+
),
|
|
148
|
+
"source_counts": _source_counts(sources),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def emit_context_pack(
|
|
153
|
+
root: str | Path,
|
|
154
|
+
pack: dict[str, Any],
|
|
155
|
+
*,
|
|
156
|
+
selection_policy: dict[str, Any] | None = None,
|
|
157
|
+
actor: str = "agent",
|
|
158
|
+
scope: str = "project",
|
|
159
|
+
) -> EmittedContextPack:
|
|
160
|
+
session_id = pack.get("session_id")
|
|
161
|
+
if not session_id:
|
|
162
|
+
raise AgentDirError("Context pack emission requires an active or explicit session")
|
|
163
|
+
manifest = build_context_manifest(pack, selection_policy=selection_policy)
|
|
164
|
+
artifact_file = _write_manifest_temp(manifest)
|
|
165
|
+
try:
|
|
166
|
+
artifact = add_artifact(root, artifact_file)
|
|
167
|
+
finally:
|
|
168
|
+
artifact_file.unlink(missing_ok=True)
|
|
169
|
+
body = "\n".join(
|
|
170
|
+
[
|
|
171
|
+
f"pack_id={manifest['pack_id']}",
|
|
172
|
+
f"protocol={CONTEXT_PROTOCOL}",
|
|
173
|
+
f"task={manifest['task']}",
|
|
174
|
+
f"session_id={session_id}",
|
|
175
|
+
f"sources={len(manifest['sources'])}",
|
|
176
|
+
f"artifact_sha256={artifact.sha256}",
|
|
177
|
+
"",
|
|
178
|
+
manifest["enforcement_boundary"],
|
|
179
|
+
]
|
|
180
|
+
)
|
|
181
|
+
headers: dict[str, str | list[str]] = {
|
|
182
|
+
HEADER_PROTOCOL: CONTEXT_PROTOCOL,
|
|
183
|
+
HEADER_PACK_ID: manifest["pack_id"],
|
|
184
|
+
HEADER_CONTEXT_QUERY: manifest["task"],
|
|
185
|
+
HEADER_CONTEXT_SCOPE: scope,
|
|
186
|
+
HEADER_ENFORCEMENT_MODE: CONTEXT_ENFORCEMENT_MODE,
|
|
187
|
+
HEADER_SOURCE_ID: [source["source_id"] for source in manifest["sources"]],
|
|
188
|
+
**artifact_headers(artifact),
|
|
189
|
+
}
|
|
190
|
+
event = emit_event(
|
|
191
|
+
root,
|
|
192
|
+
session_id=session_id,
|
|
193
|
+
event_type=EVENT_CONTEXT_PACK_CREATED,
|
|
194
|
+
subject=f"context pack: {manifest['task']}",
|
|
195
|
+
body=body,
|
|
196
|
+
from_actor=actor,
|
|
197
|
+
extra_headers=headers,
|
|
198
|
+
)
|
|
199
|
+
return EmittedContextPack(
|
|
200
|
+
manifest=manifest,
|
|
201
|
+
event_path=event.path,
|
|
202
|
+
artifact_sha256=artifact.sha256,
|
|
203
|
+
artifact_path=artifact.path,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def read_context_manifest(root: str | Path, pack_id: str, *, rebuild: bool = True) -> dict[str, Any]:
|
|
208
|
+
event = _context_event(root, pack_id, EVENT_CONTEXT_PACK_CREATED, rebuild=rebuild)
|
|
209
|
+
sha = event["headers"].get("X-AgentDir-Blob-SHA256")
|
|
210
|
+
if not sha:
|
|
211
|
+
raise AgentDirError(f"Context pack has no manifest artifact: {pack_id}")
|
|
212
|
+
manifest_file = artifact_path(root, sha)
|
|
213
|
+
if not manifest_file.is_file():
|
|
214
|
+
raise AgentDirError(f"Context pack manifest artifact is missing: {sha}")
|
|
215
|
+
manifest = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
216
|
+
if manifest.get("protocol") != CONTEXT_PROTOCOL:
|
|
217
|
+
raise AgentDirError(f"Unsupported context pack protocol: {manifest.get('protocol')}")
|
|
218
|
+
return manifest
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def consume_context_sources(
|
|
222
|
+
root: str | Path,
|
|
223
|
+
*,
|
|
224
|
+
pack_id: str,
|
|
225
|
+
source_ids: list[str],
|
|
226
|
+
purpose: str,
|
|
227
|
+
session_id: str | None = None,
|
|
228
|
+
actor: str = "agent",
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
if purpose not in CONSUMPTION_PURPOSES:
|
|
231
|
+
raise AgentDirError(f"Unknown consumption purpose: {purpose}")
|
|
232
|
+
manifest = read_context_manifest(root, pack_id)
|
|
233
|
+
selected = _select_manifest_sources(manifest, source_ids)
|
|
234
|
+
resolved_session = session_id or manifest.get("session_id")
|
|
235
|
+
if not resolved_session:
|
|
236
|
+
raise AgentDirError("Context consumption requires a session")
|
|
237
|
+
body = _format_source_event_body("context_consumed", pack_id, purpose, selected)
|
|
238
|
+
event = emit_event(
|
|
239
|
+
root,
|
|
240
|
+
session_id=resolved_session,
|
|
241
|
+
event_type=EVENT_CONTEXT_PACK_CONSUMED,
|
|
242
|
+
subject=f"context consumed: {pack_id}",
|
|
243
|
+
body=body,
|
|
244
|
+
from_actor=actor,
|
|
245
|
+
extra_headers={
|
|
246
|
+
HEADER_PROTOCOL: CONTEXT_PROTOCOL,
|
|
247
|
+
HEADER_PACK_ID: pack_id,
|
|
248
|
+
HEADER_SOURCE_ID: [source["source_id"] for source in selected],
|
|
249
|
+
HEADER_CONSUMPTION_PURPOSE: purpose,
|
|
250
|
+
HEADER_ENFORCEMENT_MODE: CONTEXT_ENFORCEMENT_MODE,
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
return {
|
|
254
|
+
"pack_id": pack_id,
|
|
255
|
+
"purpose": purpose,
|
|
256
|
+
"source_ids": [source["source_id"] for source in selected],
|
|
257
|
+
"event_path": str(event.path),
|
|
258
|
+
"enforcement_mode": CONTEXT_ENFORCEMENT_MODE,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def cite_context_sources(
|
|
263
|
+
root: str | Path,
|
|
264
|
+
*,
|
|
265
|
+
pack_id: str,
|
|
266
|
+
source_ids: list[str] | None = None,
|
|
267
|
+
output_format: str = "md",
|
|
268
|
+
session_id: str | None = None,
|
|
269
|
+
actor: str = "agent",
|
|
270
|
+
) -> dict[str, Any]:
|
|
271
|
+
manifest = read_context_manifest(root, pack_id)
|
|
272
|
+
selected = _select_manifest_sources(manifest, source_ids or _default_citation_sources(root, pack_id, manifest))
|
|
273
|
+
resolved_session = session_id or manifest.get("session_id")
|
|
274
|
+
if not resolved_session:
|
|
275
|
+
raise AgentDirError("Context citation requires a session")
|
|
276
|
+
citation = {
|
|
277
|
+
"protocol": CONTEXT_PROTOCOL,
|
|
278
|
+
"pack_id": pack_id,
|
|
279
|
+
"generated_at": now_iso(),
|
|
280
|
+
"enforcement_mode": CONTEXT_ENFORCEMENT_MODE,
|
|
281
|
+
"sources": selected,
|
|
282
|
+
"source_counts": _source_counts(selected),
|
|
283
|
+
}
|
|
284
|
+
rendered = format_context_citation(citation, output_format=output_format)
|
|
285
|
+
event = emit_event(
|
|
286
|
+
root,
|
|
287
|
+
session_id=resolved_session,
|
|
288
|
+
event_type=EVENT_CONTEXT_SOURCES_CITED,
|
|
289
|
+
subject=f"context cited: {pack_id}",
|
|
290
|
+
body=rendered,
|
|
291
|
+
from_actor=actor,
|
|
292
|
+
extra_headers={
|
|
293
|
+
HEADER_PROTOCOL: CONTEXT_PROTOCOL,
|
|
294
|
+
HEADER_PACK_ID: pack_id,
|
|
295
|
+
HEADER_SOURCE_ID: [source["source_id"] for source in selected],
|
|
296
|
+
HEADER_ENFORCEMENT_MODE: CONTEXT_ENFORCEMENT_MODE,
|
|
297
|
+
},
|
|
298
|
+
)
|
|
299
|
+
citation["event_path"] = str(event.path)
|
|
300
|
+
citation["rendered"] = format_context_citation(citation, output_format=output_format)
|
|
301
|
+
return citation
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def audit_context_pack(root: str | Path, pack_id: str, *, rebuild: bool = True) -> dict[str, Any]:
|
|
305
|
+
if rebuild:
|
|
306
|
+
rebuild_index(root)
|
|
307
|
+
manifest = read_context_manifest(root, pack_id, rebuild=False)
|
|
308
|
+
events = _context_events(root, pack_id, rebuild=False)
|
|
309
|
+
consumed = _unique_source_ids(
|
|
310
|
+
source_id
|
|
311
|
+
for event in events
|
|
312
|
+
if event["event_type"] == EVENT_CONTEXT_PACK_CONSUMED
|
|
313
|
+
for source_id in event["source_ids"]
|
|
314
|
+
)
|
|
315
|
+
cited = _unique_source_ids(
|
|
316
|
+
source_id
|
|
317
|
+
for event in events
|
|
318
|
+
if event["event_type"] == EVENT_CONTEXT_SOURCES_CITED
|
|
319
|
+
for source_id in event["source_ids"]
|
|
320
|
+
)
|
|
321
|
+
source_by_id = {source["source_id"]: source for source in manifest["sources"]}
|
|
322
|
+
evidence_backed = [
|
|
323
|
+
source_id
|
|
324
|
+
for source_id in cited
|
|
325
|
+
if source_by_id.get(source_id, {}).get("source_class") == "evidence"
|
|
326
|
+
]
|
|
327
|
+
return {
|
|
328
|
+
"protocol": CONTEXT_PROTOCOL,
|
|
329
|
+
"pack_id": pack_id,
|
|
330
|
+
"task": manifest.get("task"),
|
|
331
|
+
"session_id": manifest.get("session_id"),
|
|
332
|
+
"retrieved_count": len(manifest["sources"]),
|
|
333
|
+
"consumed_count": len(consumed),
|
|
334
|
+
"cited_count": len(cited),
|
|
335
|
+
"evidence_backed_count": len(evidence_backed),
|
|
336
|
+
"source_counts": manifest.get("source_counts", {}),
|
|
337
|
+
"consumed_source_ids": consumed,
|
|
338
|
+
"cited_source_ids": cited,
|
|
339
|
+
"evidence_backed_source_ids": evidence_backed,
|
|
340
|
+
"events": events,
|
|
341
|
+
"enforcement_mode": CONTEXT_ENFORCEMENT_MODE,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def format_context_pack(pack: dict[str, Any]) -> str:
|
|
346
|
+
lines = [
|
|
347
|
+
"# AgentDir Context Pack",
|
|
348
|
+
"",
|
|
349
|
+
f"Task: {pack['task']}",
|
|
350
|
+
f"Session: {pack.get('session_id') or 'none'}",
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
summary = pack.get("current_summary")
|
|
354
|
+
if summary:
|
|
355
|
+
lines.extend(["", "## Current Session", "", fenced(format_summary(summary), "text")])
|
|
356
|
+
|
|
357
|
+
lines.extend(["", "## Relevant Memory"])
|
|
358
|
+
memory_hits = pack.get("memory_hits") or []
|
|
359
|
+
if memory_hits:
|
|
360
|
+
for row in memory_hits:
|
|
361
|
+
lines.extend(
|
|
362
|
+
[
|
|
363
|
+
"",
|
|
364
|
+
f"- score={row.get('memory_score')} source={row.get('source_id')} kind={row.get('source_kind')}",
|
|
365
|
+
f" session={row.get('session_id') or ''} event={row.get('event_type') or ''}",
|
|
366
|
+
f" subject={row.get('subject') or ''}",
|
|
367
|
+
f" excerpt={excerpt(row.get('body_text') or '', 320)}",
|
|
368
|
+
]
|
|
369
|
+
)
|
|
370
|
+
else:
|
|
371
|
+
lines.extend(["", "No relevant memory found."])
|
|
372
|
+
|
|
373
|
+
evidence = pack.get("evidence") or []
|
|
374
|
+
if evidence:
|
|
375
|
+
lines.extend(["", "## Current Evidence", "", fenced(format_evidence(evidence), "text")])
|
|
376
|
+
|
|
377
|
+
recent = pack.get("recent_session_summaries") or []
|
|
378
|
+
if recent:
|
|
379
|
+
lines.extend(["", "## Recent Session Summaries"])
|
|
380
|
+
for row in recent:
|
|
381
|
+
lines.extend(
|
|
382
|
+
[
|
|
383
|
+
"",
|
|
384
|
+
f"- source={row.get('source_id')} session={row.get('session_id') or ''}",
|
|
385
|
+
f" {excerpt(row.get('body_text') or '', 420)}",
|
|
386
|
+
]
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
lines.extend(["", "## Agent Instructions"])
|
|
390
|
+
for instruction in pack["instructions"]:
|
|
391
|
+
lines.append(f"- {instruction}")
|
|
392
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def write_context_pack(path: str | Path, pack: dict[str, Any], *, as_json: bool = False) -> Path:
|
|
396
|
+
target = Path(path).expanduser()
|
|
397
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
398
|
+
if as_json:
|
|
399
|
+
target.write_text(json.dumps(pack, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
400
|
+
else:
|
|
401
|
+
target.write_text(format_context_pack(pack), encoding="utf-8")
|
|
402
|
+
return target
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def format_context_citation(citation: dict[str, Any], *, output_format: str = "md") -> str:
|
|
406
|
+
if output_format == "json":
|
|
407
|
+
payload = {key: value for key, value in citation.items() if key != "rendered"}
|
|
408
|
+
return json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
|
409
|
+
if output_format != "md":
|
|
410
|
+
raise AgentDirError(f"Unknown citation format: {output_format}")
|
|
411
|
+
lines = [
|
|
412
|
+
"# AgentDir Context Citations",
|
|
413
|
+
"",
|
|
414
|
+
f"Pack: {citation['pack_id']}",
|
|
415
|
+
f"Enforcement: {citation['enforcement_mode']}",
|
|
416
|
+
"",
|
|
417
|
+
]
|
|
418
|
+
for source in citation["sources"]:
|
|
419
|
+
lines.append(
|
|
420
|
+
f"- `{source['source_class']}` `{source['source_id']}` "
|
|
421
|
+
f"{source.get('event_type') or ''} {source.get('subject') or ''}".rstrip()
|
|
422
|
+
)
|
|
423
|
+
if source.get("file_path"):
|
|
424
|
+
lines.append(f" file: `{source['file_path']}`")
|
|
425
|
+
if source.get("excerpt"):
|
|
426
|
+
lines.append(f" excerpt: {source['excerpt']}")
|
|
427
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def format_context_audit(audit: dict[str, Any]) -> str:
|
|
431
|
+
lines = [
|
|
432
|
+
f"pack_id={audit['pack_id']}",
|
|
433
|
+
f"task={audit.get('task') or ''}",
|
|
434
|
+
f"session={audit.get('session_id') or ''}",
|
|
435
|
+
f"retrieved={audit['retrieved_count']}",
|
|
436
|
+
f"consumed={audit['consumed_count']}",
|
|
437
|
+
f"cited={audit['cited_count']}",
|
|
438
|
+
f"evidence_backed={audit['evidence_backed_count']}",
|
|
439
|
+
f"enforcement={audit['enforcement_mode']}",
|
|
440
|
+
]
|
|
441
|
+
for event in audit["events"]:
|
|
442
|
+
lines.append(
|
|
443
|
+
f"{event.get('date_utc') or event.get('indexed_at') or ''} "
|
|
444
|
+
f"{event['event_type']} sources={len(event['source_ids'])}"
|
|
445
|
+
)
|
|
446
|
+
return "\n".join(lines)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def fenced(text: str, language: str) -> str:
|
|
450
|
+
return f"```{language}\n{text}\n```"
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def excerpt(text: str, limit: int) -> str:
|
|
454
|
+
collapsed = " ".join(text.strip().split())
|
|
455
|
+
if len(collapsed) <= limit:
|
|
456
|
+
return collapsed
|
|
457
|
+
return collapsed[: limit - 3] + "..."
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def now_iso() -> str:
|
|
461
|
+
return datetime.now(UTC).isoformat()
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def new_pack_id() -> str:
|
|
465
|
+
stamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
|
|
466
|
+
return f"ctx-{stamp}-{uuid4().hex[:12]}"
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _source_entry(row: dict[str, Any], *, origin: str) -> dict[str, Any]:
|
|
470
|
+
source_id = _source_id(row)
|
|
471
|
+
body = row.get("body_text") or ""
|
|
472
|
+
return {
|
|
473
|
+
"source_id": source_id,
|
|
474
|
+
"source_kind": row.get("source_kind") or "message",
|
|
475
|
+
"source_class": _source_class(row, origin),
|
|
476
|
+
"origin": origin,
|
|
477
|
+
"message_id": row.get("message_id"),
|
|
478
|
+
"message_rowid": row.get("message_rowid") or row.get("id"),
|
|
479
|
+
"session_id": row.get("session_id"),
|
|
480
|
+
"event_type": row.get("event_type"),
|
|
481
|
+
"subject": row.get("subject"),
|
|
482
|
+
"from_actor": row.get("from_actor"),
|
|
483
|
+
"to_actor": row.get("to_actor"),
|
|
484
|
+
"task_id": row.get("task_id"),
|
|
485
|
+
"tool": row.get("tool"),
|
|
486
|
+
"tool_exit_code": row.get("tool_exit_code"),
|
|
487
|
+
"git_head": row.get("git_head"),
|
|
488
|
+
"workspace": row.get("workspace"),
|
|
489
|
+
"source_root_id": row.get("source_root_id"),
|
|
490
|
+
"source_root_name": row.get("source_root_name"),
|
|
491
|
+
"source_root_path": row.get("source_root_path"),
|
|
492
|
+
"source_root_visibility": row.get("source_root_visibility"),
|
|
493
|
+
"source_id_original": row.get("source_id_original"),
|
|
494
|
+
"source_file_path": row.get("source_file_path"),
|
|
495
|
+
"date_utc": row.get("date_utc"),
|
|
496
|
+
"indexed_at": row.get("indexed_at"),
|
|
497
|
+
"file_path": row.get("file_path"),
|
|
498
|
+
"memory_score": row.get("memory_score"),
|
|
499
|
+
"body_sha256": row.get("body_sha256"),
|
|
500
|
+
"text_sha256": row.get("text_sha256"),
|
|
501
|
+
"excerpt": excerpt(body, 320),
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def _source_id(row: dict[str, Any]) -> str:
|
|
506
|
+
if row.get("source_id"):
|
|
507
|
+
return str(row["source_id"])
|
|
508
|
+
if row.get("file_path"):
|
|
509
|
+
return f"message:{row['file_path']}"
|
|
510
|
+
if row.get("message_id"):
|
|
511
|
+
return f"message-id:{row['message_id']}"
|
|
512
|
+
return f"source:{uuid4().hex}"
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def _source_class(row: dict[str, Any], origin: str) -> str:
|
|
516
|
+
event_type = str(row.get("event_type") or "")
|
|
517
|
+
source_kind = str(row.get("source_kind") or "")
|
|
518
|
+
if origin == "evidence" or event_type in EVIDENCE_EVENTS or event_type.startswith("git.hook."):
|
|
519
|
+
return "evidence"
|
|
520
|
+
if source_kind == "session_summary" or event_type == "summary.compacted":
|
|
521
|
+
return "summary"
|
|
522
|
+
return "retrieval_hint"
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _dedupe_sources(sources: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
526
|
+
seen: set[str] = set()
|
|
527
|
+
deduped: list[dict[str, Any]] = []
|
|
528
|
+
for source in sources:
|
|
529
|
+
source_id = source["source_id"]
|
|
530
|
+
if source_id in seen:
|
|
531
|
+
continue
|
|
532
|
+
seen.add(source_id)
|
|
533
|
+
deduped.append(source)
|
|
534
|
+
return deduped
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def _source_counts(sources: list[dict[str, Any]]) -> dict[str, int]:
|
|
538
|
+
counts = {"evidence": 0, "retrieval_hint": 0, "summary": 0}
|
|
539
|
+
for source in sources:
|
|
540
|
+
source_class = source.get("source_class") or "retrieval_hint"
|
|
541
|
+
counts[source_class] = counts.get(source_class, 0) + 1
|
|
542
|
+
return counts
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _write_manifest_temp(manifest: dict[str, Any]) -> Path:
|
|
546
|
+
handle = tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False, suffix=".json")
|
|
547
|
+
with handle:
|
|
548
|
+
json.dump(manifest, handle, indent=2, sort_keys=True)
|
|
549
|
+
handle.write("\n")
|
|
550
|
+
return Path(handle.name)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _context_event(root: str | Path, pack_id: str, event_type: str, *, rebuild: bool = True) -> dict[str, Any]:
|
|
554
|
+
events = _context_events(root, pack_id, event_type=event_type, rebuild=rebuild)
|
|
555
|
+
if not events:
|
|
556
|
+
raise AgentDirError(f"Unknown context pack: {pack_id}")
|
|
557
|
+
return events[-1]
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _context_events(
|
|
561
|
+
root: str | Path,
|
|
562
|
+
pack_id: str,
|
|
563
|
+
*,
|
|
564
|
+
event_type: str | None = None,
|
|
565
|
+
rebuild: bool = True,
|
|
566
|
+
) -> list[dict[str, Any]]:
|
|
567
|
+
if rebuild:
|
|
568
|
+
rebuild_index(root)
|
|
569
|
+
clauses = ["hp.name = ?", "hp.value = ?"]
|
|
570
|
+
params: list[Any] = [HEADER_PACK_ID, pack_id]
|
|
571
|
+
if event_type:
|
|
572
|
+
clauses.append("m.event_type = ?")
|
|
573
|
+
params.append(event_type)
|
|
574
|
+
sql = f"""
|
|
575
|
+
select distinct m.*
|
|
576
|
+
from messages m
|
|
577
|
+
join headers hp on hp.message_rowid = m.id
|
|
578
|
+
where {' and '.join(clauses)}
|
|
579
|
+
order by coalesce(m.date_utc, m.indexed_at), coalesce(m.created_ns, 0), m.file_path, m.id
|
|
580
|
+
"""
|
|
581
|
+
with connect_index(root) as conn:
|
|
582
|
+
rows = [dict(row) for row in conn.execute(sql, params).fetchall()]
|
|
583
|
+
for row in rows:
|
|
584
|
+
headers = conn.execute(
|
|
585
|
+
"select name, value from headers where message_rowid = ? order by rowid",
|
|
586
|
+
(row["id"],),
|
|
587
|
+
).fetchall()
|
|
588
|
+
row["headers"] = _header_map(headers)
|
|
589
|
+
row["source_ids"] = [header["value"] for header in headers if header["name"] == HEADER_SOURCE_ID]
|
|
590
|
+
return rows
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def _header_map(rows: list[Any]) -> dict[str, str]:
|
|
594
|
+
mapped: dict[str, str] = {}
|
|
595
|
+
for row in rows:
|
|
596
|
+
name = row["name"]
|
|
597
|
+
value = row["value"]
|
|
598
|
+
if name not in mapped:
|
|
599
|
+
mapped[name] = value
|
|
600
|
+
return mapped
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def _select_manifest_sources(
|
|
604
|
+
manifest: dict[str, Any],
|
|
605
|
+
source_ids: list[str],
|
|
606
|
+
) -> list[dict[str, Any]]:
|
|
607
|
+
if not source_ids:
|
|
608
|
+
raise AgentDirError("At least one context source is required")
|
|
609
|
+
source_by_id = {source["source_id"]: source for source in manifest["sources"]}
|
|
610
|
+
missing = [source_id for source_id in source_ids if source_id not in source_by_id]
|
|
611
|
+
if missing:
|
|
612
|
+
raise AgentDirError("Unknown context source: " + ", ".join(missing))
|
|
613
|
+
return [source_by_id[source_id] for source_id in _unique_source_ids(source_ids)]
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _default_citation_sources(root: str | Path, pack_id: str, manifest: dict[str, Any]) -> list[str]:
|
|
617
|
+
consumed = audit_context_pack(root, pack_id)["consumed_source_ids"]
|
|
618
|
+
if consumed:
|
|
619
|
+
return consumed
|
|
620
|
+
return [source["source_id"] for source in manifest["sources"]]
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _unique_source_ids(source_ids: Any) -> list[str]:
|
|
624
|
+
seen: set[str] = set()
|
|
625
|
+
unique: list[str] = []
|
|
626
|
+
for source_id in source_ids:
|
|
627
|
+
if source_id in seen:
|
|
628
|
+
continue
|
|
629
|
+
seen.add(source_id)
|
|
630
|
+
unique.append(source_id)
|
|
631
|
+
return unique
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _format_source_event_body(
|
|
635
|
+
action: str,
|
|
636
|
+
pack_id: str,
|
|
637
|
+
purpose: str | None,
|
|
638
|
+
sources: list[dict[str, Any]],
|
|
639
|
+
) -> str:
|
|
640
|
+
lines = [
|
|
641
|
+
f"action={action}",
|
|
642
|
+
f"pack_id={pack_id}",
|
|
643
|
+
]
|
|
644
|
+
if purpose:
|
|
645
|
+
lines.append(f"purpose={purpose}")
|
|
646
|
+
lines.append(f"sources={len(sources)}")
|
|
647
|
+
lines.append("")
|
|
648
|
+
for source in sources:
|
|
649
|
+
lines.append(
|
|
650
|
+
f"- {source['source_class']} {source['source_id']} "
|
|
651
|
+
f"{source.get('event_type') or ''} {source.get('subject') or ''}".rstrip()
|
|
652
|
+
)
|
|
653
|
+
lines.extend(["", "Context protocol is advisory; source use is recorded for cooperative agents."])
|
|
654
|
+
return "\n".join(lines)
|