agent-devkit 0.0.4 → 0.1.5

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 (60) hide show
  1. package/README.md +42 -2
  2. package/package.json +1 -1
  3. package/runtime/README.md +50 -2
  4. package/runtime/agent +29 -13
  5. package/runtime/agents/README.md +3 -0
  6. package/runtime/agents/github-pr-reviewer/AGENTS.md +10 -0
  7. package/runtime/agents/github-pr-reviewer/README.md +7 -0
  8. package/runtime/agents/github-pr-reviewer/agent.yaml +33 -0
  9. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/capability.yaml +20 -0
  10. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/decision-rules.md +8 -0
  11. package/runtime/agents/github-pr-reviewer/capabilities/create-review-automation/workflow.md +9 -0
  12. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/capability.yaml +20 -0
  13. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/decision-rules.md +7 -0
  14. package/runtime/agents/github-pr-reviewer/capabilities/inspect-pr/workflow.md +8 -0
  15. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/capability.yaml +20 -0
  16. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/decision-rules.md +7 -0
  17. package/runtime/agents/github-pr-reviewer/capabilities/list-review-requests/workflow.md +8 -0
  18. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/capability.yaml +20 -0
  19. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/decision-rules.md +9 -0
  20. package/runtime/agents/github-pr-reviewer/capabilities/review-pr-diff/workflow.md +10 -0
  21. package/runtime/agents/github-pr-reviewer/infra/README.md +7 -0
  22. package/runtime/agents/github-pr-reviewer/knowledge/context.md +7 -0
  23. package/runtime/agents/github-pr-reviewer/knowledge/system.md +17 -0
  24. package/runtime/agents/github-pr-reviewer/templates/pr-automation-output.md +10 -0
  25. package/runtime/agents/github-pr-reviewer/templates/pr-inspection-output.md +9 -0
  26. package/runtime/agents/github-pr-reviewer/templates/pr-list-output.md +9 -0
  27. package/runtime/agents/github-pr-reviewer/templates/pr-review-output.md +17 -0
  28. package/runtime/cli/README.md +37 -1
  29. package/runtime/cli/aikit/__init__.py +1 -1
  30. package/runtime/cli/aikit/aliases.py +196 -0
  31. package/runtime/cli/aikit/app_home.py +78 -0
  32. package/runtime/cli/aikit/audit.py +344 -0
  33. package/runtime/cli/aikit/calendar.py +163 -0
  34. package/runtime/cli/aikit/configuration_orchestrator.py +291 -0
  35. package/runtime/cli/aikit/decision_store.py +158 -0
  36. package/runtime/cli/aikit/diagnostics.py +9 -0
  37. package/runtime/cli/aikit/fallback.py +48 -2
  38. package/runtime/cli/aikit/github_pr.py +254 -0
  39. package/runtime/cli/aikit/identity.py +75 -0
  40. package/runtime/cli/aikit/llm.py +249 -48
  41. package/runtime/cli/aikit/main.py +1240 -32
  42. package/runtime/cli/aikit/memory.py +148 -8
  43. package/runtime/cli/aikit/model_router.py +70 -0
  44. package/runtime/cli/aikit/notifications.py +9 -0
  45. package/runtime/cli/aikit/ollama.py +237 -0
  46. package/runtime/cli/aikit/permissions.py +345 -0
  47. package/runtime/cli/aikit/personality.py +123 -0
  48. package/runtime/cli/aikit/provider_wizard.py +19 -0
  49. package/runtime/cli/aikit/providers.py +4 -5
  50. package/runtime/cli/aikit/review_gate.py +40 -0
  51. package/runtime/cli/aikit/scheduler.py +18 -0
  52. package/runtime/cli/aikit/sessions.py +396 -0
  53. package/runtime/cli/aikit/setup_wizard.py +12 -0
  54. package/runtime/cli/aikit/tasks.py +351 -0
  55. package/runtime/cli/aikit/toolchain.py +274 -0
  56. package/runtime/providers/calendar.yaml +28 -0
  57. package/runtime/providers/github.yaml +42 -0
  58. package/runtime/providers/local-notification.yaml +19 -0
  59. package/runtime/providers/local-scheduler.yaml +24 -0
  60. package/runtime/vendor/skills/napkin/napkin.md +13 -3
@@ -0,0 +1,396 @@
1
+ """Persistent conversation session helpers for Agent DevKit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import uuid
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from cli.aikit.app_home import app_path, ensure_app_home, sessions_home
13
+ from cli.aikit.memory import redact_secrets
14
+
15
+
16
+ SESSION_ID_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9_.-]*$")
17
+ SESSION_STATE_VERSION = 1
18
+ RECENT_CONTEXT_EXCHANGES = 6
19
+
20
+
21
+ def now_iso() -> str:
22
+ return datetime.now(timezone.utc).isoformat()
23
+
24
+
25
+ def active_session_path() -> Path:
26
+ return app_path("state", "active-session.json")
27
+
28
+
29
+ def ensure_sessions_home() -> Path:
30
+ ensure_app_home()
31
+ home = sessions_home()
32
+ home.mkdir(parents=True, exist_ok=True)
33
+ return home
34
+
35
+
36
+ def create_session(
37
+ *,
38
+ prompt: str | None = None,
39
+ project: str | None = None,
40
+ backend: str | None = None,
41
+ set_active: bool = True,
42
+ ) -> dict[str, Any]:
43
+ home = ensure_sessions_home()
44
+ created_at = now_iso()
45
+ session_id = f"sess_{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}_{uuid.uuid4().hex[:8]}"
46
+ path = home / session_id
47
+ path.mkdir(parents=True, exist_ok=False)
48
+
49
+ state = {
50
+ "version": SESSION_STATE_VERSION,
51
+ "id": session_id,
52
+ "title": title_from_prompt(prompt),
53
+ "status": "active",
54
+ "created_at": created_at,
55
+ "updated_at": created_at,
56
+ "project": project,
57
+ "backend": backend,
58
+ "message_count": 0,
59
+ "exchange_count": 0,
60
+ "token_estimate": 0,
61
+ "path": str(path),
62
+ "summary_path": str(path / "summary.md"),
63
+ "messages_path": str(path / "messages.jsonl"),
64
+ }
65
+ write_state(path, state)
66
+ (path / "messages.jsonl").write_text("", encoding="utf-8")
67
+ write_summary(path, state, latest_prompt=prompt, latest_response=None)
68
+ write_session_markdown(path, state)
69
+ if set_active:
70
+ set_active_session(session_id)
71
+ return public_session(state, active=set_active)
72
+
73
+
74
+ def get_or_create_session(
75
+ *,
76
+ session_id: str | None = None,
77
+ force_new: bool = False,
78
+ prompt: str | None = None,
79
+ project: str | None = None,
80
+ backend: str | None = None,
81
+ ) -> dict[str, Any]:
82
+ if session_id and force_new:
83
+ raise ValueError("--session and --new-session cannot be used together")
84
+ if session_id:
85
+ state = load_session(session_id)
86
+ set_active_session(state["id"])
87
+ return public_session(state, active=True)
88
+ if force_new:
89
+ return create_session(prompt=prompt, project=project, backend=backend, set_active=True)
90
+
91
+ active_id = get_active_session_id()
92
+ if active_id:
93
+ try:
94
+ state = load_session(active_id)
95
+ if project and state.get("project") and state.get("project") != project:
96
+ return create_session(prompt=prompt, project=project, backend=backend, set_active=True)
97
+ if project and not state.get("project"):
98
+ state["project"] = project
99
+ write_state(session_path_from_id(state["id"]), state)
100
+ return public_session(state, active=True)
101
+ except ValueError:
102
+ clear_active_session()
103
+ return create_session(prompt=prompt, project=project, backend=backend, set_active=True)
104
+
105
+
106
+ def list_sessions() -> dict[str, Any]:
107
+ home = ensure_sessions_home()
108
+ active_id = get_active_session_id()
109
+ items: list[dict[str, Any]] = []
110
+ for state_path in sorted(home.glob("*/state.json")):
111
+ try:
112
+ state = read_json(state_path)
113
+ except ValueError:
114
+ continue
115
+ if not isinstance(state, dict):
116
+ continue
117
+ items.append(public_session(state, active=state.get("id") == active_id))
118
+ items.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True)
119
+ return {
120
+ "kind": "sessions",
121
+ "status": "ok",
122
+ "home": str(home),
123
+ "active_session_id": active_id,
124
+ "items": items,
125
+ }
126
+
127
+
128
+ def show_session(session_id: str) -> dict[str, Any]:
129
+ state = load_session(session_id)
130
+ return {
131
+ "kind": "session",
132
+ "status": "ok",
133
+ "session": public_session(state, active=state["id"] == get_active_session_id()),
134
+ "summary": read_text(Path(state["summary_path"])),
135
+ "recent_messages": recent_exchanges(state["id"], limit=RECENT_CONTEXT_EXCHANGES),
136
+ }
137
+
138
+
139
+ def resume_session(session_id: str) -> dict[str, Any]:
140
+ state = load_session(session_id)
141
+ set_active_session(state["id"])
142
+ return {
143
+ "kind": "session",
144
+ "status": "resumed",
145
+ "session": public_session(state, active=True),
146
+ "summary": read_text(Path(state["summary_path"])),
147
+ "recent_messages": recent_exchanges(state["id"], limit=RECENT_CONTEXT_EXCHANGES),
148
+ }
149
+
150
+
151
+ def record_exchange(
152
+ session_id: str,
153
+ *,
154
+ prompt: str,
155
+ result: dict[str, Any],
156
+ backend: str | None = None,
157
+ ) -> dict[str, Any]:
158
+ state = load_session(session_id)
159
+ path = session_path_from_id(state["id"])
160
+ response = str(result.get("response") or result.get("message") or "")
161
+ token_delta = estimate_tokens(prompt) + estimate_tokens(response)
162
+ exchange = {
163
+ "type": "exchange",
164
+ "created_at": now_iso(),
165
+ "prompt": redact_secrets(prompt),
166
+ "response": redact_secrets(response),
167
+ "status": result.get("status"),
168
+ "ok": result.get("ok"),
169
+ "backend": result.get("llm_backend") or backend,
170
+ "requires_llm": result.get("requires_llm"),
171
+ "token_estimate": token_delta,
172
+ }
173
+ with (path / "messages.jsonl").open("a", encoding="utf-8") as file:
174
+ json.dump(exchange, file, ensure_ascii=False, sort_keys=True)
175
+ file.write("\n")
176
+
177
+ state["updated_at"] = exchange["created_at"]
178
+ state["message_count"] = int(state.get("message_count") or 0) + 2
179
+ state["exchange_count"] = int(state.get("exchange_count") or 0) + 1
180
+ state["token_estimate"] = int(state.get("token_estimate") or 0) + token_delta
181
+ if backend and not state.get("backend"):
182
+ state["backend"] = backend
183
+ if state.get("title") == "Untitled session":
184
+ state["title"] = title_from_prompt(prompt)
185
+ write_state(path, state)
186
+ write_summary(path, state, latest_prompt=prompt, latest_response=response)
187
+ write_session_markdown(path, state)
188
+ set_active_session(state["id"])
189
+ return public_session(state, active=True)
190
+
191
+
192
+ def build_contextual_prompt(session_id: str, prompt: str) -> str:
193
+ exchanges = recent_exchanges(session_id, limit=RECENT_CONTEXT_EXCHANGES)
194
+ if not exchanges:
195
+ return prompt
196
+ lines = [
197
+ "Contexto recente da sessao atual do Agent DevKit:",
198
+ ]
199
+ for item in exchanges:
200
+ previous_prompt = compact_line(str(item.get("prompt") or ""))
201
+ previous_response = compact_line(str(item.get("response") or ""))
202
+ if previous_prompt:
203
+ lines.append(f"- Usuario: {previous_prompt}")
204
+ if previous_response:
205
+ lines.append(f" Assistente: {previous_response}")
206
+ lines.extend(["", "Pedido atual do usuario:", prompt])
207
+ return "\n".join(lines)
208
+
209
+
210
+ def load_session(session_id: str) -> dict[str, Any]:
211
+ resolved_id = resolve_session_id(session_id)
212
+ path = session_path_from_id(resolved_id)
213
+ state_path = path / "state.json"
214
+ if not state_path.exists():
215
+ raise ValueError(f"session not found: {session_id}")
216
+ state = read_json(state_path)
217
+ if not isinstance(state, dict):
218
+ raise ValueError(f"invalid session state: {session_id}")
219
+ state.setdefault("id", resolved_id)
220
+ state.setdefault("path", str(path))
221
+ state.setdefault("summary_path", str(path / "summary.md"))
222
+ state.setdefault("messages_path", str(path / "messages.jsonl"))
223
+ return state
224
+
225
+
226
+ def resolve_session_id(session_id: str) -> str:
227
+ if not session_id:
228
+ raise ValueError("session id is required")
229
+ if not SESSION_ID_PATTERN.match(session_id):
230
+ raise ValueError(f"invalid session id: {session_id}")
231
+ home = ensure_sessions_home()
232
+ direct = home / session_id
233
+ if direct.exists():
234
+ return session_id
235
+ matches = [path.name for path in home.iterdir() if path.is_dir() and path.name.startswith(session_id)]
236
+ if len(matches) == 1:
237
+ return matches[0]
238
+ if len(matches) > 1:
239
+ raise ValueError(f"ambiguous session id prefix: {session_id}")
240
+ raise ValueError(f"session not found: {session_id}")
241
+
242
+
243
+ def session_path_from_id(session_id: str) -> Path:
244
+ return ensure_sessions_home() / session_id
245
+
246
+
247
+ def set_active_session(session_id: str) -> None:
248
+ ensure_app_home()
249
+ path = active_session_path()
250
+ path.parent.mkdir(parents=True, exist_ok=True)
251
+ payload = {"version": SESSION_STATE_VERSION, "session_id": session_id, "updated_at": now_iso()}
252
+ with path.open("w", encoding="utf-8") as file:
253
+ json.dump(payload, file, ensure_ascii=False, indent=2)
254
+ file.write("\n")
255
+
256
+
257
+ def get_active_session_id() -> str | None:
258
+ path = active_session_path()
259
+ if not path.exists():
260
+ return None
261
+ try:
262
+ data = read_json(path)
263
+ except ValueError:
264
+ return None
265
+ if not isinstance(data, dict):
266
+ return None
267
+ session_id = data.get("session_id")
268
+ if not isinstance(session_id, str) or not SESSION_ID_PATTERN.match(session_id):
269
+ return None
270
+ if not (sessions_home() / session_id / "state.json").exists():
271
+ return None
272
+ return session_id
273
+
274
+
275
+ def clear_active_session() -> None:
276
+ path = active_session_path()
277
+ if path.exists():
278
+ path.unlink()
279
+
280
+
281
+ def public_session(state: dict[str, Any], *, active: bool = False) -> dict[str, Any]:
282
+ return {
283
+ "id": state.get("id"),
284
+ "title": state.get("title") or "Untitled session",
285
+ "status": state.get("status") or "active",
286
+ "created_at": state.get("created_at"),
287
+ "updated_at": state.get("updated_at"),
288
+ "project": state.get("project"),
289
+ "backend": state.get("backend"),
290
+ "message_count": int(state.get("message_count") or 0),
291
+ "exchange_count": int(state.get("exchange_count") or 0),
292
+ "token_estimate": int(state.get("token_estimate") or 0),
293
+ "path": state.get("path"),
294
+ "active": active,
295
+ }
296
+
297
+
298
+ def recent_exchanges(session_id: str, *, limit: int) -> list[dict[str, Any]]:
299
+ path = session_path_from_id(resolve_session_id(session_id)) / "messages.jsonl"
300
+ if not path.exists():
301
+ return []
302
+ items: list[dict[str, Any]] = []
303
+ for line in path.read_text(encoding="utf-8").splitlines():
304
+ if not line.strip():
305
+ continue
306
+ try:
307
+ item = json.loads(line)
308
+ except json.JSONDecodeError:
309
+ continue
310
+ if isinstance(item, dict):
311
+ items.append(item)
312
+ return items[-limit:]
313
+
314
+
315
+ def title_from_prompt(prompt: str | None) -> str:
316
+ if not prompt:
317
+ return "Untitled session"
318
+ text = compact_line(redact_secrets(prompt))
319
+ words = text.split()
320
+ if not words:
321
+ return "Untitled session"
322
+ title = " ".join(words[:8])
323
+ return title[:80]
324
+
325
+
326
+ def estimate_tokens(text: str | None) -> int:
327
+ if not text:
328
+ return 0
329
+ compact = " ".join(str(text).split())
330
+ return max(1, (len(compact) + 3) // 4)
331
+
332
+
333
+ def compact_line(value: str, *, limit: int = 600) -> str:
334
+ text = " ".join(value.split())
335
+ if len(text) <= limit:
336
+ return text
337
+ return text[: limit - 3].rstrip() + "..."
338
+
339
+
340
+ def write_state(path: Path, state: dict[str, Any]) -> None:
341
+ with (path / "state.json").open("w", encoding="utf-8") as file:
342
+ json.dump(state, file, ensure_ascii=False, indent=2, sort_keys=True)
343
+ file.write("\n")
344
+
345
+
346
+ def write_summary(path: Path, state: dict[str, Any], *, latest_prompt: str | None, latest_response: str | None) -> None:
347
+ lines = [
348
+ f"# {state.get('title') or 'Untitled session'}",
349
+ "",
350
+ f"- Session: {state.get('id')}",
351
+ f"- Status: {state.get('status') or 'active'}",
352
+ f"- Created: {state.get('created_at') or '-'}",
353
+ f"- Updated: {state.get('updated_at') or '-'}",
354
+ f"- Project: {state.get('project') or '-'}",
355
+ f"- Backend: {state.get('backend') or '-'}",
356
+ f"- Exchanges: {state.get('exchange_count') or 0}",
357
+ f"- Token estimate: {state.get('token_estimate') or 0}",
358
+ "",
359
+ "## Latest",
360
+ "",
361
+ f"- Prompt: {compact_line(redact_secrets(latest_prompt or '-'))}",
362
+ f"- Response: {compact_line(redact_secrets(latest_response or '-'))}",
363
+ "",
364
+ ]
365
+ (path / "summary.md").write_text("\n".join(lines), encoding="utf-8")
366
+
367
+
368
+ def write_session_markdown(path: Path, state: dict[str, Any]) -> None:
369
+ lines = [
370
+ f"# {state.get('title') or 'Untitled session'}",
371
+ "",
372
+ "This folder stores a local Agent DevKit conversation session.",
373
+ "",
374
+ "## Files",
375
+ "",
376
+ "- `state.json`: machine-readable metadata.",
377
+ "- `summary.md`: human-readable rolling summary.",
378
+ "- `messages.jsonl`: append-only redacted exchanges.",
379
+ "",
380
+ ]
381
+ (path / "session.md").write_text("\n".join(lines), encoding="utf-8")
382
+
383
+
384
+ def read_json(path: Path) -> Any:
385
+ try:
386
+ with path.open(encoding="utf-8") as file:
387
+ return json.load(file)
388
+ except (OSError, json.JSONDecodeError) as exc:
389
+ raise ValueError(f"cannot read JSON file: {path}") from exc
390
+
391
+
392
+ def read_text(path: Path) -> str:
393
+ try:
394
+ return path.read_text(encoding="utf-8")
395
+ except OSError:
396
+ return ""
@@ -0,0 +1,12 @@
1
+ """Setup wizard orchestration for Agent DevKit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from cli.aikit.toolchain import setup_plan
9
+
10
+
11
+ def setup_wizard(root: Path, *, dry_run: bool = False, yes: bool = False) -> dict[str, Any]:
12
+ return setup_plan(root, dry_run=dry_run, yes=yes)