drydock-cli 3.0.124__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.124/drydock_cli.egg-info → drydock_cli-3.0.126}/PKG-INFO +1 -1
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/agent.py +16 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/config.py +3 -0
- drydock_cli-3.0.126/drydock/events.py +68 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tui/app.py +3 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126/drydock_cli.egg-info}/PKG-INFO +1 -1
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock_cli.egg-info/SOURCES.txt +2 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/pyproject.toml +1 -1
- drydock_cli-3.0.126/tests/test_events.py +58 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_failure_loop.py +3 -3
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/LICENSE +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/NOTICE +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/README.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/__init__.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/__main__.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/advisor.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/bash_safety.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/__init__.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-data.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-debug.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-finetune.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-metrics.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-rl.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/ml-train.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-categorize.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-control.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-poam.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/rmf-review.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/stig-assess.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/builtin_skills/stig-remediate.md +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/cci.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/cli.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/compaction.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/detect.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/extract.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/gittools.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/graphrag.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/guards.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/loop_detect.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/mcp.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/poam.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/providers.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/recipes.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/rmf.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/rmf_graph.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/skills.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/stig.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/suggest.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/task_state.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tool_registry.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tools/__init__.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tui/__init__.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tui/approval.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tui/messages.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tui/widgets.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/tuning.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/verification.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock/web.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock_cli.egg-info/dependency_links.txt +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock_cli.egg-info/entry_points.txt +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock_cli.egg-info/requires.txt +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/drydock_cli.egg-info/top_level.txt +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/setup.cfg +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_advisor.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_approval.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_back_command.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_background.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_binary_output.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_crossplatform.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_output_bounding.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_process_group.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_safety.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_sanitize.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_shell.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_stdin.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_stop_partial.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_timeout_network.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_bash_timeout_param.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_cci.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_cli_agents.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_compact_command.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_compaction.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_config.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_config_migration.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_context_limit_config.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_detect.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_dispatch.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_e2e_connected.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_edit_replace_all.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_empty_response.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_extract.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_first_run_setup.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_gittools.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_glob_edit_edges.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_graphify_example.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_graphrag.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_graphrag_quoted_path.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_grep_and_read_robust.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_guards_and_tools.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_hallucinated_tools.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_leaked_tool_call.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_loop_detect.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_mcp.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_oneshot_unreachable.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_overthink_interrupt.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_plan_autocontinue.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_poam.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_providers_unreachable.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_read_index.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_recipes.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_repetition_interrupt.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_rmf.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_rmf_graph.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_rmf_stig_graph.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_runaway_repetition.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_screenshot.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_server_probe.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_skills.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_stall_retry.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_stig.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_stop.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_streaming_newlines.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_subagent.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_suggest.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_system_prompt_help.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_task_state.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_todo.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tool_arg_coercion.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tool_arg_coercion_more.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tool_arg_parsing.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tools_undo.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tui.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_tuning.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_verification_gate.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_viewimage.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_vision_input.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_web_tools.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_windows_shell.py +0 -0
- {drydock_cli-3.0.124 → drydock_cli-3.0.126}/tests/test_write_content_coerce.py +0 -0
- {drydock_cli-3.0.124 → 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
|
|
@@ -341,12 +349,14 @@ def run(
|
|
|
341
349
|
# wedge; once it runs any check (ran_verification) the gate is satisfied.
|
|
342
350
|
if (
|
|
343
351
|
config.get("verify_gate", True)
|
|
352
|
+
and allow is None # main task only — not scoped sub-agents
|
|
344
353
|
and session_has_edited
|
|
345
354
|
and not ran_verification
|
|
346
355
|
and verify_gate_nudges < 2
|
|
347
356
|
):
|
|
348
357
|
verify_gate_nudges += 1
|
|
349
358
|
state.task.phase = "verify"
|
|
359
|
+
_emit(state, "verify_gate", nudge=verify_gate_nudges)
|
|
350
360
|
state.messages.append({
|
|
351
361
|
"role": "user",
|
|
352
362
|
"content": (
|
|
@@ -361,6 +371,8 @@ def run(
|
|
|
361
371
|
continue
|
|
362
372
|
if session_has_edited and ran_verification:
|
|
363
373
|
state.task.phase = "complete"
|
|
374
|
+
_emit(state, "done", phase=state.task.phase, edited=session_has_edited,
|
|
375
|
+
verified=ran_verification)
|
|
364
376
|
break
|
|
365
377
|
|
|
366
378
|
# Execute each tool call
|
|
@@ -423,6 +435,10 @@ def run(
|
|
|
423
435
|
# advisory note when the same call is made again.
|
|
424
436
|
result = loop_tracker.annotate(tc["name"], tc["input"], result)
|
|
425
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"))
|
|
426
442
|
yield ToolEnd(tc["name"], result)
|
|
427
443
|
|
|
428
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
|
|
@@ -38,7 +38,7 @@ def test_changing_args_do_not_trip_valve(monkeypatch):
|
|
|
38
38
|
monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
|
|
39
39
|
st = AgentState()
|
|
40
40
|
import tempfile
|
|
41
|
-
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
41
|
+
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp(), "verify_gate": False}, "sys"))
|
|
42
42
|
assert any(m.get("role") == "assistant" and m.get("content") == "done"
|
|
43
43
|
for m in st.messages)
|
|
44
44
|
|
|
@@ -57,7 +57,7 @@ def test_identical_SUCCESS_streak_ends_turn(monkeypatch):
|
|
|
57
57
|
|
|
58
58
|
monkeypatch.setattr(agent_mod, "stream", counting)
|
|
59
59
|
st = AgentState()
|
|
60
|
-
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
60
|
+
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp(), "verify_gate": False}, "sys"))
|
|
61
61
|
assert calls["n"] <= 12 # identical success → stopped at the cap, not 200
|
|
62
62
|
|
|
63
63
|
|
|
@@ -73,7 +73,7 @@ def test_polling_with_changing_result_not_tripped(monkeypatch):
|
|
|
73
73
|
seq.append(AssistantTurn("done", [], 1, 1))
|
|
74
74
|
monkeypatch.setattr(agent_mod, "stream", lambda **kw: iter([seq.pop(0)]))
|
|
75
75
|
st = AgentState()
|
|
76
|
-
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp()}, "sys"))
|
|
76
|
+
list(run("go", st, {"model": "m", "cwd": tempfile.mkdtemp(), "verify_gate": False}, "sys"))
|
|
77
77
|
assert any(m.get("content") == "done" for m in st.messages)
|
|
78
78
|
|
|
79
79
|
|
|
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
|