Skip to content

Commit 487d231

Browse files
authored
Merge pull request #87 from LAA-Software-Engineering/feat/trace-retention-days
Enforce spec.traces.retentionDays for SQLite traces (lazy GC)
2 parents d2f6781 + 567b48a commit 487d231

17 files changed

Lines changed: 270 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ Notes:
135135
- **`init`** creates `my-agent-system/` with `apiVersion: agentic.dev/v0` resources and a **`hello`** workflow (native `echo` tool only — **no network**).
136136
- **`apply`** in non-interactive environments needs **`--auto-approve`** or **`AGENTCTL_AUTO_APPROVE=1`**.
137137
- **`run`** stores traces in the **same** SQLite file used for plan/apply (default **`.agentic/state.db`** under `--project`).
138+
- If **`spec.traces.retentionDays`** is a positive integer, runs older than that many **UTC calendar days** (by `runs.started_at`) are deleted lazily on **`run`** and **`logs`** (child trace rows cascade). Unset or non-positive means no pruning.
138139
- Use **`logs --run <id>`** after a run if you want a single run’s trace (IDs are printed by **`run`**).
139140

140141
### Global flags (common)

internal/cli/logs.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"time"
1212

1313
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/render"
14+
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/spec"
1415
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/state"
1516
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/state/sqlite"
1617
"github.com/spf13/cobra"
@@ -76,6 +77,13 @@ func runLogs(cmd *cobra.Command, runID, workflow string) error {
7677
}
7778
defer func() { _ = st.Close() }()
7879

80+
if n := spec.TraceRetentionDays(graph); n > 0 {
81+
cutoff := time.Now().UTC().AddDate(0, 0, -n)
82+
if _, err := st.DeleteRunsStartedBefore(ctx, cutoff); err != nil {
83+
return fmt.Errorf("logs: prune trace runs: %w", err)
84+
}
85+
}
86+
7987
switch {
8088
case runID != "":
8189
return writeLogsForRun(cmd, ctx, st, dsn, runID, g)

internal/runtime/local/runner.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,13 @@ func (r *Runtime) ExecuteWorkflow(ctx context.Context, opts runtime.WorkflowRunO
6666
return "", err
6767
}
6868

69+
if n := spec.TraceRetentionDays(graph); n > 0 {
70+
cutoff := r.now().UTC().AddDate(0, 0, -n)
71+
if _, err := r.Store.DeleteRunsStartedBefore(ctx, cutoff); err != nil {
72+
return "", fmt.Errorf("local: prune trace runs: %w", err)
73+
}
74+
}
75+
6976
runID := strings.TrimSpace(opts.RunID)
7077
if runID == "" {
7178
runID = util.NewRunID()

internal/runtime/local/runner_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package local
22

33
import (
44
"context"
5+
"database/sql"
6+
"errors"
57
"path/filepath"
68
"testing"
9+
"time"
710

811
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/project"
912
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/runtime"
13+
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/state"
1014
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/state/sqlite"
1115
"github.com/LAA-Software-Engineering/agentic-control-plane/internal/trace"
1216
)
@@ -16,6 +20,11 @@ func testRunProjRoot(t *testing.T) string {
1620
return filepath.Join("testdata", "runproj")
1721
}
1822

23+
func testRetentionProjRoot(t *testing.T) string {
24+
t.Helper()
25+
return filepath.Join("testdata", "retention")
26+
}
27+
1928
func TestExecuteWorkflow_persistsRunAndTraceInSQLite(t *testing.T) {
2029
ctx := context.Background()
2130
st, err := sqlite.Open(ctx, filepath.Join(t.TempDir(), "localrun.db"))
@@ -147,3 +156,47 @@ func TestNewRunID_generatedWhenEmpty(t *testing.T) {
147156
t.Fatal(err)
148157
}
149158
}
159+
160+
func TestExecuteWorkflow_prunesOldTraceRuns(t *testing.T) {
161+
ctx := context.Background()
162+
st, err := sqlite.Open(ctx, filepath.Join(t.TempDir(), "retention.db"))
163+
if err != nil {
164+
t.Fatal(err)
165+
}
166+
t.Cleanup(func() { _ = st.Close() })
167+
168+
fixed := time.Date(2026, 4, 12, 12, 0, 0, 0, time.UTC)
169+
oldID := "stale-run"
170+
oldStart := fixed.Add(-72 * time.Hour)
171+
if err := st.StartRun(ctx, state.Run{
172+
RunID: oldID, WorkflowName: "demo", Env: "local", Status: "succeeded",
173+
StartedAt: oldStart, InputJSON: `{}`, TotalCostUSD: 0,
174+
}); err != nil {
175+
t.Fatal(err)
176+
}
177+
if _, err := st.AppendTraceEvent(ctx, oldID, oldStart, trace.EventRunStarted, "", `{}`); err != nil {
178+
t.Fatal(err)
179+
}
180+
181+
rt := NewRuntime(testRetentionProjRoot(t), st)
182+
rt.Now = func() time.Time { return fixed }
183+
184+
newID := "fresh-run"
185+
_, err = rt.ExecuteWorkflow(ctx, runtime.WorkflowRunOptions{
186+
RunID: newID,
187+
WorkflowName: "demo",
188+
EnvironmentName: "staging",
189+
Env: "dev",
190+
InputJSON: []byte(`{"topic":"p"}`),
191+
})
192+
if err != nil {
193+
t.Fatal(err)
194+
}
195+
196+
if _, err := st.GetRun(ctx, oldID); !errors.Is(err, sql.ErrNoRows) {
197+
t.Fatalf("old run: %v", err)
198+
}
199+
if _, err := st.GetRun(ctx, newID); err != nil {
200+
t.Fatal(err)
201+
}
202+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: agentic.dev/v0
2+
kind: Agent
3+
metadata:
4+
name: reviewer
5+
spec:
6+
model: mock/gpt-4
7+
instructions: Summarize the tool payload as JSON.
8+
output:
9+
schema: ./schemas/agent-out.schema.json
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: agentic.dev/v0
2+
kind: Environment
3+
metadata:
4+
name: staging
5+
spec:
6+
overrides:
7+
agents:
8+
reviewer:
9+
constraints:
10+
timeoutSeconds: 99
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: agentic.dev/v0
2+
kind: Project
3+
metadata:
4+
name: runproj
5+
spec:
6+
imports:
7+
- ./tools.yaml
8+
- ./agents.yaml
9+
- ./workflows.yaml
10+
- ./environments.yaml
11+
providers:
12+
models:
13+
mock:
14+
type: mock
15+
traces:
16+
backend: sqlite
17+
retentionDays: 1
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"type": "object",
4+
"required": ["summary"],
5+
"properties": {
6+
"summary": { "type": "string" }
7+
},
8+
"additionalProperties": true
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"type": "object",
4+
"required": ["topic"],
5+
"properties": {
6+
"topic": { "type": "string" }
7+
},
8+
"additionalProperties": true
9+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: agentic.dev/v0
2+
kind: Tool
3+
metadata:
4+
name: helper
5+
spec:
6+
type: native

0 commit comments

Comments
 (0)