agent-self-edit 0.2.0__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.
- agent_self_edit/__init__.py +8 -0
- agent_self_edit/__main__.py +4 -0
- agent_self_edit/ab_test.py +333 -0
- agent_self_edit/adapters/__init__.py +7 -0
- agent_self_edit/adapters/base.py +27 -0
- agent_self_edit/adapters/file.py +63 -0
- agent_self_edit/adapters/stdin.py +48 -0
- agent_self_edit/analyzer.py +665 -0
- agent_self_edit/cli/__init__.py +43 -0
- agent_self_edit/cli/diff.py +36 -0
- agent_self_edit/cli/guardrails.py +45 -0
- agent_self_edit/cli/ingest.py +44 -0
- agent_self_edit/cli/init.py +59 -0
- agent_self_edit/cli/lineage.py +32 -0
- agent_self_edit/cli/propose.py +142 -0
- agent_self_edit/cli/rollback.py +25 -0
- agent_self_edit/cli/run.py +142 -0
- agent_self_edit/cli/status.py +76 -0
- agent_self_edit/cli/validate.py +81 -0
- agent_self_edit/config.py +242 -0
- agent_self_edit/diff.py +298 -0
- agent_self_edit/gate.py +485 -0
- agent_self_edit/guardrails.py +334 -0
- agent_self_edit/llm/__init__.py +7 -0
- agent_self_edit/llm/base.py +26 -0
- agent_self_edit/llm/mock.py +64 -0
- agent_self_edit/llm/openai.py +142 -0
- agent_self_edit/registry.py +419 -0
- agent_self_edit/scorers.py +375 -0
- agent_self_edit/tasks.py +199 -0
- agent_self_edit/trace.py +360 -0
- agent_self_edit/types.py +182 -0
- agent_self_edit-0.2.0.dist-info/METADATA +198 -0
- agent_self_edit-0.2.0.dist-info/RECORD +38 -0
- agent_self_edit-0.2.0.dist-info/WHEEL +5 -0
- agent_self_edit-0.2.0.dist-info/entry_points.txt +2 -0
- agent_self_edit-0.2.0.dist-info/licenses/LICENSE +21 -0
- agent_self_edit-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"""Task runner, A/B test engine, statistics, and cost tracking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import random
|
|
6
|
+
import statistics
|
|
7
|
+
import time
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from .config import ABTestConfig, Config
|
|
12
|
+
from .llm.base import LLMProvider, ProviderError
|
|
13
|
+
from .scorers import Scorer
|
|
14
|
+
from .tasks import Task, TaskSet
|
|
15
|
+
from .types import utc_now_iso # noqa: F401 (re-export convenience)
|
|
16
|
+
|
|
17
|
+
_COST_PER_1K_TOKENS = 0.0033 # approx gpt-4o-mini blend ($ per 1K tokens)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class TaskResult:
|
|
22
|
+
"""Result of running one task against one prompt."""
|
|
23
|
+
|
|
24
|
+
output: str
|
|
25
|
+
success: bool
|
|
26
|
+
latency_ms: float
|
|
27
|
+
token_count: int
|
|
28
|
+
error: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class PerTask:
|
|
33
|
+
"""Paired per-task comparison for the A/B result."""
|
|
34
|
+
|
|
35
|
+
task_id: str
|
|
36
|
+
task_input: str
|
|
37
|
+
expected_output: str
|
|
38
|
+
output_a: str
|
|
39
|
+
score_a: float
|
|
40
|
+
output_b: str
|
|
41
|
+
score_b: float
|
|
42
|
+
delta: float
|
|
43
|
+
latency_a_ms: float
|
|
44
|
+
latency_b_ms: float
|
|
45
|
+
tokens_a: int
|
|
46
|
+
tokens_b: int
|
|
47
|
+
error_a: str | None = None
|
|
48
|
+
error_b: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class ABResult:
|
|
53
|
+
"""Statistical result of comparing prompt B against prompt A."""
|
|
54
|
+
|
|
55
|
+
winner: Literal["a", "b", "tie", "inconclusive"]
|
|
56
|
+
mean_delta: float
|
|
57
|
+
ci_low: float
|
|
58
|
+
ci_high: float
|
|
59
|
+
p_value: float
|
|
60
|
+
effect_size: float
|
|
61
|
+
n_trials: int
|
|
62
|
+
per_task: list[PerTask] = field(default_factory=list)
|
|
63
|
+
cost_usd: float = 0.0
|
|
64
|
+
token_count: int = 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class BootstrapResult:
|
|
69
|
+
mean: float
|
|
70
|
+
ci_low: float
|
|
71
|
+
ci_high: float
|
|
72
|
+
std: float
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def estimate_tokens(text: str) -> int:
|
|
76
|
+
"""Rough token estimate: ~4 characters per token."""
|
|
77
|
+
return max(1, len(text) // 4)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def estimate_cost(token_count: int, price_per_1k: float = _COST_PER_1K_TOKENS) -> float:
|
|
81
|
+
"""Estimate USD cost for ``token_count`` tokens."""
|
|
82
|
+
return token_count * price_per_1k / 1000.0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Task runner (#16)
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def run_task(task: Task, prompt: str, llm: LLMProvider) -> TaskResult:
|
|
91
|
+
"""Run ``task`` against one ``prompt`` and measure latency + tokens."""
|
|
92
|
+
if not prompt.strip():
|
|
93
|
+
return TaskResult(output="", success=False, latency_ms=0.0, token_count=0,
|
|
94
|
+
error="empty prompt")
|
|
95
|
+
|
|
96
|
+
full_prompt = f"{prompt}\n\n---\n\nTask: {task.input}"
|
|
97
|
+
start = time.monotonic()
|
|
98
|
+
try:
|
|
99
|
+
output = llm.complete(prompt=full_prompt, system_prompt="", temperature=0.0)
|
|
100
|
+
except ProviderError as e:
|
|
101
|
+
return TaskResult(output="", success=False, latency_ms=0.0, token_count=0,
|
|
102
|
+
error=str(e))
|
|
103
|
+
latency_ms = (time.monotonic() - start) * 1000.0
|
|
104
|
+
tokens = estimate_tokens(full_prompt) + estimate_tokens(output)
|
|
105
|
+
return TaskResult(
|
|
106
|
+
output=output,
|
|
107
|
+
success=True,
|
|
108
|
+
latency_ms=latency_ms,
|
|
109
|
+
token_count=tokens,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Statistics (#19, #20, #21)
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def bootstrap_ci(
|
|
119
|
+
scores_a: list[float],
|
|
120
|
+
scores_b: list[float],
|
|
121
|
+
n_resamples: int = 10000,
|
|
122
|
+
ci_level: float = 0.95,
|
|
123
|
+
) -> BootstrapResult:
|
|
124
|
+
"""Bootstrap CI for the mean delta = mean(score_b - score_a).
|
|
125
|
+
|
|
126
|
+
Uses a deterministic seed for reproducibility.
|
|
127
|
+
"""
|
|
128
|
+
n = len(scores_a)
|
|
129
|
+
if n == 0:
|
|
130
|
+
return BootstrapResult(mean=0.0, ci_low=0.0, ci_high=0.0, std=0.0)
|
|
131
|
+
if n < 2:
|
|
132
|
+
return BootstrapResult(mean=0.0, ci_low=0.0, ci_high=0.0, std=0.0)
|
|
133
|
+
|
|
134
|
+
deltas = [b - a for a, b in zip(scores_a, scores_b)]
|
|
135
|
+
mean_delta = sum(deltas) / n
|
|
136
|
+
|
|
137
|
+
if n_resamples <= 0:
|
|
138
|
+
return BootstrapResult(mean=mean_delta, ci_low=mean_delta, ci_high=mean_delta, std=0.0)
|
|
139
|
+
|
|
140
|
+
rng = random.Random(0)
|
|
141
|
+
means: list[float] = []
|
|
142
|
+
for _ in range(n_resamples):
|
|
143
|
+
sample = [rng.choice(deltas) for _ in range(n)]
|
|
144
|
+
means.append(sum(sample) / n)
|
|
145
|
+
|
|
146
|
+
means.sort()
|
|
147
|
+
tail = (1.0 - ci_level) / 2.0
|
|
148
|
+
low_idx = int(tail * n_resamples)
|
|
149
|
+
high_idx = int((1.0 - tail) * n_resamples) - 1
|
|
150
|
+
ci_low = means[low_idx]
|
|
151
|
+
ci_high = means[high_idx]
|
|
152
|
+
std = statistics.stdev(means) if len(means) > 1 else 0.0
|
|
153
|
+
return BootstrapResult(mean=mean_delta, ci_low=ci_low, ci_high=ci_high, std=std)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def permutation_test(
|
|
157
|
+
scores_a: list[float],
|
|
158
|
+
scores_b: list[float],
|
|
159
|
+
n_permutations: int = 1000,
|
|
160
|
+
) -> float:
|
|
161
|
+
"""One-tailed permutation p-value: how often a random split beats observed.
|
|
162
|
+
|
|
163
|
+
Deterministic seed for reproducibility.
|
|
164
|
+
"""
|
|
165
|
+
n = len(scores_a)
|
|
166
|
+
if n == 0:
|
|
167
|
+
return 1.0
|
|
168
|
+
observed_diff = sum(scores_b) / n - sum(scores_a) / n
|
|
169
|
+
pooled = list(scores_a) + list(scores_b)
|
|
170
|
+
rng = random.Random(0)
|
|
171
|
+
|
|
172
|
+
count = 0
|
|
173
|
+
for _ in range(n_permutations):
|
|
174
|
+
rng.shuffle(pooled)
|
|
175
|
+
fake_a = pooled[:n]
|
|
176
|
+
fake_b = pooled[n:]
|
|
177
|
+
fake_diff = sum(fake_b) / n - sum(fake_a) / n
|
|
178
|
+
if fake_diff >= observed_diff:
|
|
179
|
+
count += 1
|
|
180
|
+
return count / n_permutations
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def effect_size(scores_a: list[float], scores_b: list[float]) -> float:
|
|
184
|
+
"""Relative improvement = (mean_b - mean_a) / mean_a.
|
|
185
|
+
|
|
186
|
+
Baseline of 0 renders ``inf`` for any positive improvement (handled by
|
|
187
|
+
the caller), and 0.0 for no change.
|
|
188
|
+
"""
|
|
189
|
+
if not scores_a or not scores_b:
|
|
190
|
+
return 0.0
|
|
191
|
+
mean_a = sum(scores_a) / len(scores_a)
|
|
192
|
+
mean_b = sum(scores_b) / len(scores_b)
|
|
193
|
+
if mean_a == 0:
|
|
194
|
+
if mean_b > 0:
|
|
195
|
+
return float("inf")
|
|
196
|
+
return 0.0
|
|
197
|
+
return (mean_b - mean_a) / mean_a
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ---------------------------------------------------------------------------
|
|
201
|
+
# A/B test runner (#18)
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _resolve_ab_config(config: Config | None) -> ABTestConfig:
|
|
206
|
+
return config.ab_test if config is not None else ABTestConfig()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def run_ab_test(
|
|
210
|
+
prompt_a: str,
|
|
211
|
+
prompt_b: str,
|
|
212
|
+
task_set: TaskSet,
|
|
213
|
+
llm: LLMProvider,
|
|
214
|
+
scorer: Scorer,
|
|
215
|
+
config: Config | None = None,
|
|
216
|
+
) -> ABResult:
|
|
217
|
+
"""Run the paired A/B comparison of ``prompt_b`` vs ``prompt_a``."""
|
|
218
|
+
ab_config = _resolve_ab_config(config)
|
|
219
|
+
tasks = task_set.list_tasks()
|
|
220
|
+
results: list[PerTask] = []
|
|
221
|
+
total_tokens = 0
|
|
222
|
+
failures = 0
|
|
223
|
+
|
|
224
|
+
for task in tasks:
|
|
225
|
+
result_a = run_task(task, prompt_a, llm)
|
|
226
|
+
result_b = run_task(task, prompt_b, llm)
|
|
227
|
+
|
|
228
|
+
score_a = (
|
|
229
|
+
scorer.score(task.expected_output, result_a.output)[1]
|
|
230
|
+
if not result_a.error
|
|
231
|
+
else 0.0
|
|
232
|
+
)
|
|
233
|
+
score_b = (
|
|
234
|
+
scorer.score(task.expected_output, result_b.output)[1]
|
|
235
|
+
if not result_b.error
|
|
236
|
+
else 0.0
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
if result_a.error or result_b.error:
|
|
240
|
+
failures += 1
|
|
241
|
+
|
|
242
|
+
total_tokens += result_a.token_count + result_b.token_count
|
|
243
|
+
results.append(
|
|
244
|
+
PerTask(
|
|
245
|
+
task_id=task.id,
|
|
246
|
+
task_input=task.input,
|
|
247
|
+
expected_output=task.expected_output,
|
|
248
|
+
output_a=result_a.output,
|
|
249
|
+
score_a=score_a,
|
|
250
|
+
output_b=result_b.output,
|
|
251
|
+
score_b=score_b,
|
|
252
|
+
delta=score_b - score_a,
|
|
253
|
+
latency_a_ms=result_a.latency_ms,
|
|
254
|
+
latency_b_ms=result_b.latency_ms,
|
|
255
|
+
tokens_a=result_a.token_count,
|
|
256
|
+
tokens_b=result_b.token_count,
|
|
257
|
+
error_a=result_a.error,
|
|
258
|
+
error_b=result_b.error,
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Cost ceiling abort (D3 §8.1)
|
|
263
|
+
if estimate_cost(total_tokens) > ab_config.cost_ceiling_usd:
|
|
264
|
+
return _inconclusive(results, total_tokens)
|
|
265
|
+
|
|
266
|
+
if tasks and failures / len(tasks) > 0.2:
|
|
267
|
+
return _inconclusive(results, total_tokens)
|
|
268
|
+
|
|
269
|
+
scores_a = [r.score_a for r in results]
|
|
270
|
+
scores_b = [r.score_b for r in results]
|
|
271
|
+
mean_a = sum(scores_a) / len(scores_a) if scores_a else 0.0
|
|
272
|
+
mean_b = sum(scores_b) / len(scores_b) if scores_b else 0.0
|
|
273
|
+
|
|
274
|
+
if not scores_a:
|
|
275
|
+
return _inconclusive([], 0)
|
|
276
|
+
|
|
277
|
+
deltas = [r.delta for r in results]
|
|
278
|
+
if all(d == 0.0 for d in deltas):
|
|
279
|
+
winner: Literal["a", "b", "tie", "inconclusive"] = "tie"
|
|
280
|
+
ci = BootstrapResult(mean=0.0, ci_low=0.0, ci_high=0.0, std=0.0)
|
|
281
|
+
p_value = 1.0
|
|
282
|
+
else:
|
|
283
|
+
ci = bootstrap_ci(
|
|
284
|
+
scores_a, scores_b,
|
|
285
|
+
n_resamples=ab_config.n_resamples,
|
|
286
|
+
)
|
|
287
|
+
p_value = permutation_test(
|
|
288
|
+
scores_a, scores_b,
|
|
289
|
+
n_permutations=ab_config.n_permutations,
|
|
290
|
+
)
|
|
291
|
+
effect = effect_size(scores_a, scores_b)
|
|
292
|
+
alpha = 1.0 - ab_config.confidence_level
|
|
293
|
+
if (
|
|
294
|
+
ci.ci_low > 0
|
|
295
|
+
and p_value < alpha
|
|
296
|
+
and effect >= ab_config.min_effect_size
|
|
297
|
+
):
|
|
298
|
+
winner = "b"
|
|
299
|
+
elif ci.ci_high < 0 and p_value < alpha:
|
|
300
|
+
winner = "a"
|
|
301
|
+
else:
|
|
302
|
+
winner = "inconclusive"
|
|
303
|
+
|
|
304
|
+
return ABResult(
|
|
305
|
+
winner=winner,
|
|
306
|
+
mean_delta=mean_b - mean_a,
|
|
307
|
+
ci_low=ci.ci_low,
|
|
308
|
+
ci_high=ci.ci_high,
|
|
309
|
+
p_value=p_value,
|
|
310
|
+
effect_size=effect_size(scores_a, scores_b),
|
|
311
|
+
n_trials=len(results),
|
|
312
|
+
per_task=results,
|
|
313
|
+
cost_usd=estimate_cost(total_tokens),
|
|
314
|
+
token_count=total_tokens,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _inconclusive(
|
|
319
|
+
per_task: list[PerTask],
|
|
320
|
+
token_count: int,
|
|
321
|
+
) -> ABResult:
|
|
322
|
+
return ABResult(
|
|
323
|
+
winner="inconclusive",
|
|
324
|
+
mean_delta=0.0,
|
|
325
|
+
ci_low=0.0,
|
|
326
|
+
ci_high=0.0,
|
|
327
|
+
p_value=1.0,
|
|
328
|
+
effect_size=0.0,
|
|
329
|
+
n_trials=len(per_task),
|
|
330
|
+
per_task=per_task,
|
|
331
|
+
cost_usd=estimate_cost(token_count),
|
|
332
|
+
token_count=token_count,
|
|
333
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Trace adapter interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import threading
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
|
|
8
|
+
from ..trace import TraceStore
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TraceAdapter(ABC):
|
|
12
|
+
"""Abstract adapter that ingests traces from a specific source."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, store: TraceStore) -> None:
|
|
15
|
+
self._store = store
|
|
16
|
+
self._stop_event = threading.Event()
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def run(self) -> None:
|
|
20
|
+
"""Blocking run loop; calls ``store.ingest()`` per trace."""
|
|
21
|
+
|
|
22
|
+
def stop(self) -> None:
|
|
23
|
+
"""Signal the run loop to exit."""
|
|
24
|
+
self._stop_event.set()
|
|
25
|
+
|
|
26
|
+
def _stopped(self) -> bool:
|
|
27
|
+
return self._stop_event.is_set()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Directory-watching trace adapter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from ..trace import TraceStore
|
|
11
|
+
from .base import TraceAdapter
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("agent_self_edit.adapters.file")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FileAdapter(TraceAdapter):
|
|
17
|
+
"""Watches a directory for new ``.json`` trace files and ingests each.
|
|
18
|
+
|
|
19
|
+
Each file must be a JSON object (one trace per file). After successful
|
|
20
|
+
ingestion the file is moved to a ``.done`` sibling so it is not re-read.
|
|
21
|
+
Uses the ``.done`` rename as the sole dedup mechanism — no in-memory
|
|
22
|
+
filename cache, so repeated filenames are safe (ref #142).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self, store: TraceStore, watch_dir: str | Path, poll_interval: float = 1.0
|
|
27
|
+
) -> None:
|
|
28
|
+
super().__init__(store)
|
|
29
|
+
self._watch_dir = Path(watch_dir)
|
|
30
|
+
self._watch_dir.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
self._poll_interval = poll_interval
|
|
32
|
+
|
|
33
|
+
def run(self) -> None:
|
|
34
|
+
while not self._stopped():
|
|
35
|
+
self._process_once()
|
|
36
|
+
time.sleep(self._poll_interval)
|
|
37
|
+
|
|
38
|
+
def _process_once(self) -> int:
|
|
39
|
+
processed = 0
|
|
40
|
+
for path in sorted(self._watch_dir.glob("*.json")):
|
|
41
|
+
self._ingest_file(path)
|
|
42
|
+
processed += 1
|
|
43
|
+
return processed
|
|
44
|
+
|
|
45
|
+
def _ingest_file(self, path: Path) -> None:
|
|
46
|
+
try:
|
|
47
|
+
data = json.loads(path.read_text())
|
|
48
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
49
|
+
logger.warning("Skipping malformed trace file %s: %s", path, e)
|
|
50
|
+
return
|
|
51
|
+
if not isinstance(data, dict):
|
|
52
|
+
logger.warning("Skipping trace file %s: expected a JSON object", path)
|
|
53
|
+
return
|
|
54
|
+
try:
|
|
55
|
+
self._store.ingest(data)
|
|
56
|
+
except ValueError as e:
|
|
57
|
+
logger.warning("Skipping invalid trace %s: %s", path, e)
|
|
58
|
+
return
|
|
59
|
+
try:
|
|
60
|
+
done = path.with_suffix(path.suffix + ".done")
|
|
61
|
+
path.rename(done)
|
|
62
|
+
except OSError as e:
|
|
63
|
+
logger.warning("Could not move %s to .done: %s", path, e)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Stdin-aware trace adapter for JSON-lines input."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
from typing import TextIO
|
|
8
|
+
|
|
9
|
+
from ..trace import TraceStore
|
|
10
|
+
from .base import TraceAdapter
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class StdinAdapter(TraceAdapter):
|
|
14
|
+
"""Reads JSON-lines from stdin, one trace object per line, and ingests each."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self, store: TraceStore, stream: TextIO | None = None, batch_size: int = 1000
|
|
18
|
+
) -> None:
|
|
19
|
+
super().__init__(store)
|
|
20
|
+
self._stream = stream if stream is not None else sys.stdin
|
|
21
|
+
self._batch_size = batch_size
|
|
22
|
+
self._ingested = 0
|
|
23
|
+
|
|
24
|
+
def run(self) -> None:
|
|
25
|
+
for line in self._stream:
|
|
26
|
+
if self._stopped():
|
|
27
|
+
break
|
|
28
|
+
line = line.strip()
|
|
29
|
+
if not line:
|
|
30
|
+
continue
|
|
31
|
+
try:
|
|
32
|
+
trace = json.loads(line)
|
|
33
|
+
except json.JSONDecodeError as e:
|
|
34
|
+
print(f"agent-self-edit: skipping malformed line: {e}", file=sys.stderr)
|
|
35
|
+
continue
|
|
36
|
+
try:
|
|
37
|
+
self._store.ingest(trace)
|
|
38
|
+
self._ingested += 1
|
|
39
|
+
except ValueError as e:
|
|
40
|
+
print(
|
|
41
|
+
f"agent-self-edit: skipping invalid trace: {e}",
|
|
42
|
+
file=sys.stderr,
|
|
43
|
+
)
|
|
44
|
+
if self._ingested % self._batch_size == 0:
|
|
45
|
+
print(
|
|
46
|
+
f"agent-self-edit: ingested {self._ingested} traces",
|
|
47
|
+
file=sys.stderr,
|
|
48
|
+
)
|