drydock-cli 3.0.125__tar.gz → 3.0.126__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.
- {drydock_cli-3.0.125/drydock_cli.egg-info → drydock_cli-3.0.126}/PKG-INFO +1 -1
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/agent.py +15 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/config.py +3 -0
- drydock_cli-3.0.126/drydock/events.py +68 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/tui/app.py +3 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/pyproject.toml +1 -1
- drydock_cli-3.0.126/tests/test_events.py +58 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/LICENSE +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/NOTICE +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/README.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/cci.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/cli.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/detect.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/extract.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/guards.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/poam.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/providers.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/skills.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/stig.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/task_state.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/verification.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock/web.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/setup.cfg +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_config.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_failure_loop.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.125 → drydock_cli-3.0.126}/tests/test_xccdf.py +0 -0
|
@@ -49,6 +49,7 @@ from drydock.compaction import (
|
|
|
49
49
|
from drydock.loop_detect import LoopTracker
|
|
50
50
|
from drydock.task_state import TaskState
|
|
51
51
|
from drydock.verification import looks_like_verification
|
|
52
|
+
from drydock.events import emit as _emit
|
|
52
53
|
from drydock.tuning import (
|
|
53
54
|
filter_tool_schemas,
|
|
54
55
|
hallucinated_tool_message,
|
|
@@ -83,6 +84,7 @@ class AgentState:
|
|
|
83
84
|
turn_count: int = 0
|
|
84
85
|
current_effort: str = "" # "high"/"low" of the in-flight LLM call (for the UI)
|
|
85
86
|
task: "TaskState" = field(default_factory=lambda: TaskState()) # structured objective
|
|
87
|
+
events: object | None = None # optional EventLog for a durable execution trace
|
|
86
88
|
|
|
87
89
|
|
|
88
90
|
def drop_last_turn(messages: list) -> bool:
|
|
@@ -121,6 +123,9 @@ def run(
|
|
|
121
123
|
# outside the transcript. The original objective is authoritative for the task.
|
|
122
124
|
if not state.task.is_set() and isinstance(user_message, str) and user_message.strip():
|
|
123
125
|
state.task = TaskState.from_objective(user_message)
|
|
126
|
+
_emit(state, "task_start", objective=state.task.objective,
|
|
127
|
+
acceptance_criteria=state.task.acceptance_criteria)
|
|
128
|
+
_emit(state, "user_message", chars=len(user_message or ""))
|
|
124
129
|
# Keep the objective + acceptance criteria in the SYSTEM PROMPT every turn, so
|
|
125
130
|
# they survive compaction (which only touches the message transcript, never the
|
|
126
131
|
# system prompt) — the model can't drift off the goal on a long task.
|
|
@@ -281,6 +286,9 @@ def run(
|
|
|
281
286
|
state.total_output_tokens += assistant_turn.output_tokens
|
|
282
287
|
state.last_input_tokens = assistant_turn.input_tokens
|
|
283
288
|
yield TurnDone(assistant_turn.input_tokens, assistant_turn.output_tokens)
|
|
289
|
+
_emit(state, "turn", in_tok=assistant_turn.input_tokens,
|
|
290
|
+
out_tok=assistant_turn.output_tokens,
|
|
291
|
+
tool_calls=len(assistant_turn.tool_calls or []))
|
|
284
292
|
|
|
285
293
|
# No tool calls = conversation complete — UNLESS the model emitted a
|
|
286
294
|
# tool call as text (a Gemma quirk the API can't structure). In that
|
|
@@ -348,6 +356,7 @@ def run(
|
|
|
348
356
|
):
|
|
349
357
|
verify_gate_nudges += 1
|
|
350
358
|
state.task.phase = "verify"
|
|
359
|
+
_emit(state, "verify_gate", nudge=verify_gate_nudges)
|
|
351
360
|
state.messages.append({
|
|
352
361
|
"role": "user",
|
|
353
362
|
"content": (
|
|
@@ -362,6 +371,8 @@ def run(
|
|
|
362
371
|
continue
|
|
363
372
|
if session_has_edited and ran_verification:
|
|
364
373
|
state.task.phase = "complete"
|
|
374
|
+
_emit(state, "done", phase=state.task.phase, edited=session_has_edited,
|
|
375
|
+
verified=ran_verification)
|
|
365
376
|
break
|
|
366
377
|
|
|
367
378
|
# Execute each tool call
|
|
@@ -424,6 +435,10 @@ def run(
|
|
|
424
435
|
# advisory note when the same call is made again.
|
|
425
436
|
result = loop_tracker.annotate(tc["name"], tc["input"], result)
|
|
426
437
|
|
|
438
|
+
_emit(state, "tool", name=tc["name"],
|
|
439
|
+
input=str(tc.get("input"))[:200],
|
|
440
|
+
result_chars=len(str(result)),
|
|
441
|
+
error=str(result)[:80].lower().startswith("error"))
|
|
427
442
|
yield ToolEnd(tc["name"], result)
|
|
428
443
|
|
|
429
444
|
# Append tool result
|
|
@@ -52,6 +52,9 @@ DEFAULTS: dict[str, object] = {
|
|
|
52
52
|
# files but never ran a test/check/its own code, nudge it to verify first
|
|
53
53
|
# instead of accepting completion. Bounded. Set false to disable.
|
|
54
54
|
"verify_gate": True,
|
|
55
|
+
# Durable per-session execution trace (events.py) written as JSONL to
|
|
56
|
+
# ~/.drydock/events/ — inspect/diagnose/replay a task. Set false to disable.
|
|
57
|
+
"event_log": True,
|
|
55
58
|
"theme": "harbor",
|
|
56
59
|
# Optional SECOND model ("advisor") for a stronger second opinion — e.g. a
|
|
57
60
|
# Gemini OpenAI-compatible endpoint on another box. Empty = disabled. The
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Durable execution event log (Agent-Buildout PRD, Epic: events).
|
|
2
|
+
|
|
3
|
+
Every task run appends structured events to a per-session JSONL file so a task can
|
|
4
|
+
be inspected, diagnosed, replayed, and later resumed. Append-only, one event per
|
|
5
|
+
line: ``{"seq", "ts", "type", ...}``. DEFENSIVE — a logging failure must never crash
|
|
6
|
+
the agent, so every I/O path swallows its own errors. All logic original to Drydock.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EventLog:
|
|
16
|
+
"""Append-only JSONL sink for execution events. Cheap to call; never raises."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, path):
|
|
19
|
+
self.path = Path(path)
|
|
20
|
+
self._seq = 0
|
|
21
|
+
try:
|
|
22
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
except OSError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
def emit(self, type: str, **data) -> None:
|
|
27
|
+
self._seq += 1
|
|
28
|
+
rec = {"seq": self._seq, "ts": round(time.time(), 3), "type": type}
|
|
29
|
+
rec.update(data)
|
|
30
|
+
try:
|
|
31
|
+
with self.path.open("a", encoding="utf-8") as f:
|
|
32
|
+
f.write(json.dumps(rec, default=str, ensure_ascii=False) + "\n")
|
|
33
|
+
except (OSError, TypeError, ValueError):
|
|
34
|
+
pass # logging must never break a run
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def read(path) -> list[dict]:
|
|
38
|
+
"""Parse an event-log JSONL into a list of event dicts (in order). Skips any
|
|
39
|
+
unparseable line; returns [] if the file is missing."""
|
|
40
|
+
out: list[dict] = []
|
|
41
|
+
try:
|
|
42
|
+
text = Path(path).read_text("utf-8", errors="ignore")
|
|
43
|
+
except OSError:
|
|
44
|
+
return out
|
|
45
|
+
for line in text.splitlines():
|
|
46
|
+
line = line.strip()
|
|
47
|
+
if not line:
|
|
48
|
+
continue
|
|
49
|
+
try:
|
|
50
|
+
out.append(json.loads(line))
|
|
51
|
+
except json.JSONDecodeError:
|
|
52
|
+
continue
|
|
53
|
+
return out
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def emit(state, type: str, **data) -> None:
|
|
57
|
+
"""Emit to the run's event log if it has one — a no-op otherwise. Lets the agent
|
|
58
|
+
loop log unconditionally without a log everywhere."""
|
|
59
|
+
log = getattr(state, "events", None)
|
|
60
|
+
if log is not None:
|
|
61
|
+
log.emit(type, **data)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def default_event_log_path():
|
|
65
|
+
"""A fresh per-session JSONL path under ~/.drydock/events/."""
|
|
66
|
+
import time
|
|
67
|
+
from pathlib import Path
|
|
68
|
+
return Path.home() / ".drydock" / "events" / f"session-{int(time.time())}.jsonl"
|
|
@@ -259,6 +259,9 @@ class DrydockApp(App):
|
|
|
259
259
|
super().__init__()
|
|
260
260
|
self.config = config
|
|
261
261
|
self.state = AgentState()
|
|
262
|
+
if config.get("event_log", True):
|
|
263
|
+
from drydock.events import EventLog, default_event_log_path
|
|
264
|
+
self.state.events = EventLog(config.get("event_log_path") or default_event_log_path())
|
|
262
265
|
self.system = self._build_system(config.get("model"))
|
|
263
266
|
from drydock.skills import load_skills
|
|
264
267
|
self._skills = load_skills(config.get("cwd") or ".")
|
|
@@ -12,6 +12,7 @@ drydock/cli.py
|
|
|
12
12
|
drydock/compaction.py
|
|
13
13
|
drydock/config.py
|
|
14
14
|
drydock/detect.py
|
|
15
|
+
drydock/events.py
|
|
15
16
|
drydock/extract.py
|
|
16
17
|
drydock/gittools.py
|
|
17
18
|
drydock/graphrag.py
|
|
@@ -83,6 +84,7 @@ tests/test_dispatch.py
|
|
|
83
84
|
tests/test_e2e_connected.py
|
|
84
85
|
tests/test_edit_replace_all.py
|
|
85
86
|
tests/test_empty_response.py
|
|
87
|
+
tests/test_events.py
|
|
86
88
|
tests/test_extract.py
|
|
87
89
|
tests/test_failure_loop.py
|
|
88
90
|
tests/test_first_run_setup.py
|
|
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
|
|
7
7
|
# PyPI distribution name is drydock-cli (the established install name, continued
|
|
8
8
|
# from the retired v2 fork); the import package + CLI command stay `drydock`.
|
|
9
9
|
name = "drydock-cli"
|
|
10
|
-
version = "3.0.
|
|
10
|
+
version = "3.0.126"
|
|
11
11
|
description = "Drydock — a local, provider-agnostic terminal coding agent for local LLMs"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.11"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Durable event log (Agent-Buildout PRD, Epic: events) — the run appends a JSONL
|
|
2
|
+
execution trace that can be read back for inspection/replay."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
from drydock import agent
|
|
9
|
+
from drydock.events import EventLog
|
|
10
|
+
from drydock.providers import AssistantTurn
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_emit_and_read_roundtrip():
|
|
14
|
+
p = os.path.join(tempfile.mkdtemp(), "e.jsonl")
|
|
15
|
+
log = EventLog(p)
|
|
16
|
+
log.emit("task_start", objective="do X", acceptance_criteria=["a", "b"])
|
|
17
|
+
log.emit("tool", name="Write", result_chars=10)
|
|
18
|
+
evs = EventLog.read(p)
|
|
19
|
+
assert [e["type"] for e in evs] == ["task_start", "tool"]
|
|
20
|
+
assert evs[0]["seq"] == 1 and evs[1]["seq"] == 2 and "ts" in evs[0]
|
|
21
|
+
assert evs[0]["acceptance_criteria"] == ["a", "b"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_read_missing_file_is_empty():
|
|
25
|
+
assert EventLog.read("/no/such/file.jsonl") == []
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_emit_never_raises_on_bad_path():
|
|
29
|
+
EventLog("/root/cannot/write/here.jsonl").emit("x", a=1) # no exception
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_run_writes_full_trace():
|
|
33
|
+
p = os.path.join(tempfile.mkdtemp(), "run.jsonl")
|
|
34
|
+
st = agent.AgentState(); st.events = EventLog(p)
|
|
35
|
+
turns = [
|
|
36
|
+
AssistantTurn("", [{"id": "1", "name": "Write",
|
|
37
|
+
"input": {"file_path": "/tmp/dd_e.txt", "content": "x"}}], 10, 5),
|
|
38
|
+
AssistantTurn("", [{"id": "2", "name": "Bash", "input": {"command": "python -c '1'"}}], 8, 4),
|
|
39
|
+
AssistantTurn("done, verified", [], 6, 3),
|
|
40
|
+
]
|
|
41
|
+
i = {"n": 0}
|
|
42
|
+
def fake(model, system, messages, tool_schemas, config):
|
|
43
|
+
t = turns[min(i["n"], len(turns) - 1)]; i["n"] += 1; yield t
|
|
44
|
+
orig = agent.stream; agent.stream = fake
|
|
45
|
+
try:
|
|
46
|
+
list(agent.run("Do the task.\n1. write out.txt", st,
|
|
47
|
+
{"model": "gemma4", "max_tokens": 8192, "context_limit": 65536}, "SYS"))
|
|
48
|
+
finally:
|
|
49
|
+
agent.stream = orig
|
|
50
|
+
evs = EventLog.read(p)
|
|
51
|
+
types = [e["type"] for e in evs]
|
|
52
|
+
assert types[0] == "task_start" and "turn" in types and "tool" in types and types[-1] == "done"
|
|
53
|
+
assert next(e for e in evs if e["type"] == "done")["phase"] == "complete"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_disabled_no_events_attr():
|
|
57
|
+
st = agent.AgentState()
|
|
58
|
+
assert st.events is None # off by default on a bare AgentState
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|