agent-apprenticeship 0.1.6 → 0.1.7

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 (33) hide show
  1. package/bin/agent-apprenticeship.js +85 -37
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/src/agent_apprenticeship_trace/__init__.py +1 -1
  5. package/src/agent_apprenticeship_trace/actual_outputs_normalizer.py +104 -28
  6. package/src/agent_apprenticeship_trace/apprentice_adapters.py +3 -1
  7. package/src/agent_apprenticeship_trace/artifact_resolver.py +6 -0
  8. package/src/agent_apprenticeship_trace/bundle_exporter.py +132 -2
  9. package/src/agent_apprenticeship_trace/cli.py +1171 -262
  10. package/src/agent_apprenticeship_trace/codex_runner.py +89 -6
  11. package/src/agent_apprenticeship_trace/config.py +251 -32
  12. package/src/agent_apprenticeship_trace/ecosystem_r2.py +515 -0
  13. package/src/agent_apprenticeship_trace/env.py +5 -5
  14. package/src/agent_apprenticeship_trace/evaluator.py +127 -11
  15. package/src/agent_apprenticeship_trace/grader.py +12 -11
  16. package/src/agent_apprenticeship_trace/learning.py +270 -1
  17. package/src/agent_apprenticeship_trace/llm_output_normalizer.py +8 -7
  18. package/src/agent_apprenticeship_trace/loop.py +103 -4
  19. package/src/agent_apprenticeship_trace/loop_review.py +686 -0
  20. package/src/agent_apprenticeship_trace/mentor_checkpoints.py +49 -21
  21. package/src/agent_apprenticeship_trace/openai_structured.py +84 -14
  22. package/src/agent_apprenticeship_trace/package_exporter.py +1 -1
  23. package/src/agent_apprenticeship_trace/progress.py +2 -1
  24. package/src/agent_apprenticeship_trace/public_run.py +369 -57
  25. package/src/agent_apprenticeship_trace/public_sanitizer.py +37 -1
  26. package/src/agent_apprenticeship_trace/rubric_generation.py +11 -10
  27. package/src/agent_apprenticeship_trace/schemas.py +378 -3
  28. package/src/agent_apprenticeship_trace/task_intake.py +43 -9
  29. package/src/agent_apprenticeship_trace/tdo.py +2304 -0
  30. package/src/agent_apprenticeship_trace/trace_normalizer.py +351 -17
  31. package/src/agent_apprenticeship_trace/trace_prompt.py +38 -11
  32. package/src/agent_apprenticeship_trace/training_signals.py +1 -1
  33. package/src/agent_apprenticeship_trace/verifier.py +12 -6
@@ -13,12 +13,57 @@ from .contract_diagnostics import build_contract_diagnostics, diagnostics_text
13
13
  from .trace_normalizer import TraceNormalizationContext, repair_agent_trace_file, TraceNormalizationReport
14
14
  from .actual_outputs_normalizer import ActualOutputsNormalizationContext, repair_actual_outputs_file, write_actual_outputs_normalization
15
15
  from .public_sanitizer import classify_provider_failure, public_error_summary
16
+ from .progress import utc_now
16
17
 
17
18
  class AttemptResult(dict): pass
18
19
 
19
20
  CODEX_TRUST_RETRY_MESSAGE = "Codex refused to run because the workspace is not a trusted Git directory. Retrying with --skip-git-repo-check if supported."
20
21
 
21
22
 
23
+ def _safe_agent_config(settings, runner_kind: str, attempt_kind: str) -> dict:
24
+ return {
25
+ "apprentice_agent_id": settings.worker_agent,
26
+ "apprentice_agent_name": settings.custom_worker_display_name if settings.worker_agent == "custom" else settings.worker_agent,
27
+ "apprenticeship_mode": getattr(settings, "apprenticeship_mode", None),
28
+ "mentor_mode": settings.mentor_mode,
29
+ "mentor_model_provider": settings.model_provider,
30
+ "max_improvement_loops": settings.max_improvement_loops,
31
+ "attempt_kind": attempt_kind,
32
+ "runner_kind": runner_kind,
33
+ "sensitive_info_masking": settings.sensitive_info_masking,
34
+ }
35
+
36
+
37
+ def _enrich_runtime_trace(
38
+ trace: AgentTrace,
39
+ actual: ActualOutputs,
40
+ package_root: Path,
41
+ attempt_kind: str,
42
+ runner_kind: str,
43
+ started_at: str | None,
44
+ ended_at: str | None,
45
+ ) -> AgentTrace:
46
+ settings = get_settings()
47
+ trace.run_id = package_root.parent.parent.name if package_root.parent.parent.name else None
48
+ trace.package_id = package_root.name
49
+ trace.started_at = trace.started_at or started_at
50
+ trace.ended_at = trace.ended_at or ended_at
51
+ trace.agent_config = trace.agent_config or _safe_agent_config(settings, runner_kind, attempt_kind)
52
+ trace.actual_outputs_ref = f"attempts/{attempt_kind}/actual_outputs.json"
53
+ trace.input_artifact_refs = list(dict.fromkeys(actual.input_artifact_refs or trace.input_artifact_refs or []))
54
+ trace.artifact_refs = list(dict.fromkeys((actual.artifact_refs or actual.files_created or []) + (trace.artifact_refs or [])))
55
+ trace.deliverable_refs = list(dict.fromkeys((actual.deliverable_refs or []) + (trace.deliverable_refs or [])))
56
+ trace.output_summary = trace.output_summary or actual.output_summary
57
+ trace.final_output_summary = trace.final_output_summary or actual.output_summary
58
+ trace.environment_domain = trace.environment_domain or (
59
+ "mixed"
60
+ if len({s.environment_domain for s in trace.steps if s.environment_domain and s.environment_domain != "unknown"}) > 1
61
+ else next(iter({s.environment_domain for s in trace.steps if s.environment_domain and s.environment_domain != "unknown"}), "unknown")
62
+ )
63
+ trace.metadata_json.setdefault("runtime_metadata_populated", True)
64
+ return trace
65
+
66
+
22
67
  def _run_with_process_group_timeout(command, *, cwd: Path, timeout: int | None, shell: bool = False) -> subprocess.CompletedProcess:
23
68
  process = subprocess.Popen(
24
69
  command,
@@ -53,6 +98,19 @@ def _attempt_dir(package_root: Path, attempt_kind: str) -> Path:
53
98
  (p/'input').mkdir(parents=True, exist_ok=True)
54
99
  return p
55
100
 
101
+
102
+ def _recover_misplaced_contract_files(d: Path) -> dict[str, str]:
103
+ """Recover contract files agents sometimes place under ./artifacts/."""
104
+ recovered: dict[str, str] = {}
105
+ for name in ("agent_trace.json", "actual_outputs.json"):
106
+ dst = d / name
107
+ src = d / "artifacts" / name
108
+ if dst.exists() or not src.exists() or not src.is_file():
109
+ continue
110
+ shutil.copy2(src, dst)
111
+ recovered[name] = f"artifacts/{name}"
112
+ return recovered
113
+
56
114
  def _copy_attempt_inputs(package_root: Path, attempt_dir: Path) -> list[str]:
57
115
  src=package_root/'input'; dst=attempt_dir/'input'
58
116
  dst.mkdir(parents=True, exist_ok=True)
@@ -101,9 +159,11 @@ def _fallback_actual(spec: TaskIntakeSpec, attempt_kind: str, error_type: str, e
101
159
 
102
160
  def _apprentice_operational_error(error: object | None, stdout: str = "", stderr: str = "", returncode: int | None = None) -> str | None:
103
161
  text = f"{error or ''}\n{stdout or ''}\n{stderr or ''}".lower()
162
+ if error is None and returncode == 0:
163
+ return None
104
164
  if isinstance(error, FileNotFoundError) or "no such file or directory: 'codex'" in text or "no such file or directory: codex" in text:
105
165
  return "Apprentice Agent command not found: codex"
106
- if isinstance(error, subprocess.TimeoutExpired) or "timed out" in text or "timeout" in text:
166
+ if isinstance(error, subprocess.TimeoutExpired) or "timed out" in text or "timeout expired" in text or "command timed out" in text:
107
167
  return "Apprentice Agent timed out while running Codex."
108
168
  if _is_codex_workspace_trust_error(stdout, stderr, error):
109
169
  return "Codex workspace trust error: Codex refused to run because the workspace is not a trusted Git directory. Use a Codex CLI with --skip-git-repo-check support or run from a trusted Git repository."
@@ -191,7 +251,7 @@ def _fallback_trace(spec: TaskIntakeSpec, attempt_kind: str, prompt: str, actual
191
251
  error_type=cls.get('error_type') or error_type
192
252
  steps=[
193
253
  AgentTraceStep(step=1, turn=1, actor='user', action='user_message', input=spec.normalized_instruction, message_role='direct_request'),
194
- AgentTraceStep(step=2, turn=1, actor=actor, action='error', operation='other', tool='codex_cli', observation='Codex attempt ended without a valid agent_trace.json or actual_outputs.json.', input='Validate required output contract for ./agent_trace.json and ./actual_outputs.json.', output=safe, state_change='A minimal failure trace and failed actual_outputs.json were written by the runner.', reasoning='The package must preserve failure evidence without fabricating a successful detailed trace.', caused_by=[1], causal_type='dependency_on_tool_result', success=False, step_outcome='failed', error_type=error_type, error_message=safe, artifact_refs=[f'attempts/{attempt_kind}/prompt.md', f'attempts/{attempt_kind}/stdout.txt', f'attempts/{attempt_kind}/stderr.txt', f'attempts/{attempt_kind}/final_message.txt']),
254
+ AgentTraceStep(step=2, turn=1, actor=actor, action='error', operation='other', tool='codex_cli', environment_domain='terminal', execution_mode='serial', observation='Codex attempt ended without a valid agent_trace.json or actual_outputs.json.', input='Validate required output contract for ./agent_trace.json and ./actual_outputs.json.', input_source={'source':'runtime_contract_check'}, output=safe, state_change='A minimal failure trace and failed actual_outputs.json were written by the runner.', reasoning='The package must preserve failure evidence without fabricating a successful detailed trace.', caused_by=[1], causal_type='dependency_on_tool_result', success=False, step_outcome='failed', error_type=error_type, error_message=safe, artifact_refs=[f'attempts/{attempt_kind}/prompt.md', f'attempts/{attempt_kind}/stdout.txt', f'attempts/{attempt_kind}/stderr.txt', f'attempts/{attempt_kind}/final_message.txt']),
195
255
  ]
196
256
  return AgentTrace(trace_id=f'trace_{attempt_id}_fallback', collection_id=None, trace_mode='live', task=spec.normalized_instruction, task_id=spec.task_id, attempt_id=attempt_id, attempt_kind=attempt_kind if attempt_kind in ['baseline','revised'] else 'other', agent_tools=['codex_cli','Bash','python','file_read','file_write'], system_prompt=prompt, system_prompt_hash=None, skills=['agent_trace_skill'], learning='When a live agent cannot write outputs, preserve logs and create a minimal failure trace.', termination_reason='agent_blocked', steps=steps, actual_outputs=actual, artifacts=[], metadata_json={'trace_valid':False,'fallback_trace_created':True,'codex_sandbox':codex_sandbox, **cls})
197
257
 
@@ -202,6 +262,7 @@ def _write_fallback_report(d: Path, spec: TaskIntakeSpec, attempt_kind: str, rea
202
262
 
203
263
  def ensure_attempt_outputs(package_root: Path, spec: TaskIntakeSpec, attempt_kind: str, prompt: str, codex_sandbox: str, validation_error: Exception | None=None) -> tuple[AgentTrace, ActualOutputs, bool]:
204
264
  d=_attempt_dir(package_root, attempt_kind)
265
+ recovered_contract_files = _recover_misplaced_contract_files(d)
205
266
  actual_path=d/'actual_outputs.json'; trace_path=d/'agent_trace.json'; raw_path=d/'agent_trace.raw.json'
206
267
  err = validation_error or FileNotFoundError('agent_trace.json missing or invalid')
207
268
  required_artifacts=_deliverables(RawTaskRecord(raw_task_id=spec.task_id, source_kind='normalized_spec', raw_title=spec.normalized_title, raw_description=spec.normalized_instruction, raw_payload={'expected_deliverable': spec.expected_agent_deliverable}), spec)
@@ -225,7 +286,11 @@ def ensure_attempt_outputs(package_root: Path, spec: TaskIntakeSpec, attempt_kin
225
286
  write_json(d/'agent_trace.normalized.json', result.normalized_trace)
226
287
  write_json(d/'agent_trace.json', result.normalized_trace)
227
288
  write_json(d/'trace_normalization_report.json', result.report)
228
- return AgentTrace.model_validate(result.normalized_trace), actual, bool(result.report.trace_schema_valid)
289
+ trace = AgentTrace.model_validate(result.normalized_trace)
290
+ if recovered_contract_files:
291
+ trace.metadata_json["contract_files_recovered_from"] = recovered_contract_files
292
+ actual.metadata_json.setdefault("contract_files_recovered_from", recovered_contract_files)
293
+ return trace, actual, bool(result.report.trace_schema_valid)
229
294
  reason=result.parse_error or 'parseable trace contained no steps'
230
295
  final=(d/'final_message.txt').read_text(errors='ignore') if (d/'final_message.txt').exists() else ''
231
296
  stderr=(d/'stderr.txt').read_text(errors='ignore') if (d/'stderr.txt').exists() else ''
@@ -243,6 +308,7 @@ def ensure_attempt_outputs(package_root: Path, spec: TaskIntakeSpec, attempt_kin
243
308
  return trace, actual, False
244
309
 
245
310
  def deterministic_attempt(package_root: Path, raw: RawTaskRecord, spec: TaskIntakeSpec, attempt_kind='baseline', feedback: str | None=None) -> AttemptResult:
311
+ started_at = utc_now()
246
312
  attempt_id=f'{spec.task_id}_{attempt_kind}'
247
313
  d=_attempt_dir(package_root, attempt_kind)
248
314
  input_files=_copy_attempt_inputs(package_root, d)
@@ -258,18 +324,22 @@ def deterministic_attempt(package_root: Path, raw: RawTaskRecord, spec: TaskInta
258
324
  fname=name if '.' in name and '/' not in name else name.lower().replace(' ','_') + '.txt'
259
325
  (d/'artifacts'/fname).write_text(f'{attempt_kind} deterministic artifact for {name}\n')
260
326
  files.append(f'attempts/{attempt_kind}/artifacts/{fname}')
261
- actual=ActualOutputs(task_id=spec.task_id, attempt_id=attempt_id, attempt_kind=attempt_kind, status='success', output_summary=f'Deterministic {attempt_kind} outputs for {spec.normalized_title}', primary_output_ref=files[0] if files else None, deliverable_refs=files, final_message_ref=f'attempts/{attempt_kind}/final_message.txt', artifact_refs=files, files_created=files, files_modified=[], files_deleted=[], stdout_ref=f'attempts/{attempt_kind}/stdout.txt', stderr_ref=f'attempts/{attempt_kind}/stderr.txt', raw_log_refs=[f'attempts/{attempt_kind}/stdout.txt', f'attempts/{attempt_kind}/stderr.txt'], error_type=None, error_message=None, metadata_json={'runner':'deterministic','expected_deliverable_items':deliverables,'produced_deliverable_items':[Path(f).name for f in files]})
327
+ actual=ActualOutputs(task_id=spec.task_id, attempt_id=attempt_id, attempt_kind=attempt_kind, status='success', output_summary=f'Deterministic {attempt_kind} outputs for {spec.normalized_title}', primary_output_ref=files[0] if files else None, input_artifact_refs=[f'input/{name}' for name in input_files], deliverable_refs=files, final_message_ref=f'attempts/{attempt_kind}/final_message.txt', artifact_refs=files, files_created=files, files_modified=[], files_deleted=[], stdout_ref=f'attempts/{attempt_kind}/stdout.txt', stderr_ref=f'attempts/{attempt_kind}/stderr.txt', raw_log_refs=[f'attempts/{attempt_kind}/stdout.txt', f'attempts/{attempt_kind}/stderr.txt'], error_type=None, error_message=None, metadata_json={'runner':'deterministic','expected_deliverable_items':deliverables,'produced_deliverable_items':[Path(f).name for f in files]})
262
328
  steps=[
263
329
  AgentTraceStep(step=1, turn=1, actor='user', action='user_message', input=spec.normalized_instruction, message_role='direct_request'),
264
- AgentTraceStep(step=2, turn=1, actor=f'agent:{"reviser" if attempt_kind=="revised" else "worker"}', action='agent_step', operation='plan', tool=None, observation='Task instructions, ./input files, and worker-visible rubric are available.', input='Review task and required deliverables.', output='Plan created.', state_change='The attempt plan identified required artifacts.', reasoning='A short plan reduces missed deliverables.', caused_by=[1], causal_type='user_request', success=True, step_outcome='progress'),
265
- AgentTraceStep(step=3, turn=1, actor=f'agent:{"reviser" if attempt_kind=="revised" else "worker"}', action='agent_step', operation='write', tool='file_write', observation='Deliverable names are known.', input=', '.join(files), output='Artifacts written.', state_change='Required deterministic artifacts were created under ./artifacts.', reasoning='Writing explicit files satisfies the artifact contract.', caused_by=[2], causal_type='execution_of_plan', success=True, step_outcome='completed', artifact_refs=files),
330
+ AgentTraceStep(step=2, turn=1, actor=f'agent:{"reviser" if attempt_kind=="revised" else "worker"}', action='agent_step', operation='plan', tool=None, environment_domain='file', execution_mode='serial', observation='Task instructions, ./input files, and worker-visible rubric are available.', input='Review task and required deliverables.', input_source={'source':'task_packet'}, output='Plan created.', state_change='The attempt plan identified required artifacts.', reasoning='A short plan reduces missed deliverables.', caused_by=[1], causal_type='user_request', success=True, step_outcome='progress', artifact_refs=[f'input/{name}' for name in input_files]),
331
+ AgentTraceStep(step=3, turn=1, actor=f'agent:{"reviser" if attempt_kind=="revised" else "worker"}', action='agent_step', operation='write', tool='file_write', environment_domain='file', execution_mode='serial', observation='Deliverable names are known.', input=', '.join(files), input_source={'source':'planned_deliverables'}, output='Artifacts written.', state_change='Required deterministic artifacts were created under ./artifacts.', reasoning='Writing explicit files satisfies the artifact contract.', caused_by=[2], causal_type='execution_of_plan', success=True, step_outcome='completed', artifact_refs=files),
266
332
  ]
333
+ ended_at = utc_now()
267
334
  trace=AgentTrace(trace_id=f'trace_{attempt_id}', collection_id=None, trace_mode='live', task=spec.normalized_instruction, task_id=spec.task_id, attempt_id=attempt_id, attempt_kind=attempt_kind if attempt_kind in ['baseline','revised'] else 'other', agent_tools=['deterministic_runner','file_write'], system_prompt=prompt, system_prompt_hash=None, skills=['agent_trace_skill'], learning='Materialize required artifacts and keep evaluation outside the trace.', termination_reason='task_complete', steps=steps, actual_outputs=actual, artifacts=[], metadata_json={'trace_valid': True})
335
+ trace = _enrich_runtime_trace(trace, actual, package_root, attempt_kind, 'deterministic', started_at, ended_at)
268
336
  write_json(d/'actual_outputs.json', actual)
269
337
  actual_ctx=ActualOutputsNormalizationContext(task_id=spec.task_id, attempt_id=attempt_id, attempt_kind=attempt_kind, package_root=package_root, required_artifacts=deliverables)
270
338
  actual_result=repair_actual_outputs_file(d/'actual_outputs.json', actual_ctx)
271
339
  write_actual_outputs_normalization(d, actual_result)
272
340
  actual=ActualOutputs.model_validate(actual_result.actual_outputs)
341
+ trace.actual_outputs = actual
342
+ trace = _enrich_runtime_trace(trace, actual, package_root, attempt_kind, 'deterministic', started_at, ended_at)
273
343
  write_json(d/'agent_trace.raw.json', trace); write_json(d/'agent_trace.normalized.json', trace); write_json(d/'agent_trace.json', trace)
274
344
  report=TraceNormalizationReport(task_id=spec.task_id, attempt_id=attempt_id, attempt_kind=attempt_kind, raw_trace_ref='agent_trace.raw.json', normalized_trace_ref='agent_trace.normalized.json', canonical_trace_ref='agent_trace.json', trace_schema_valid=True, trace_normalized=False, trace_lossless=True, fallback_trace=False, raw_step_count=len(steps), normalized_step_count=len(steps), discarded_step_count=0)
275
345
  write_json(d/'trace_normalization_report.json', report)
@@ -300,6 +370,7 @@ def codex_command(
300
370
  return cmd
301
371
 
302
372
  def run_codex_attempt(package_root: Path, raw: RawTaskRecord, spec: TaskIntakeSpec, attempt_kind='baseline', timeout=900) -> AttemptResult:
373
+ started_at = utc_now()
303
374
  d=_attempt_dir(package_root, attempt_kind)
304
375
  input_files=_copy_attempt_inputs(package_root, d)
305
376
  deliverables=_deliverables(raw, spec)
@@ -350,22 +421,29 @@ def run_codex_attempt(package_root: Path, raw: RawTaskRecord, spec: TaskIntakeSp
350
421
  except Exception as e:
351
422
  run_error=e
352
423
  (d/'stdout.txt').write_text(''); (d/'stderr.txt').write_text(redact_secrets(str(e))); (d/'final_message.txt').write_text('Codex run failed before producing validated trace.')
424
+ recovered_contract_files = _recover_misplaced_contract_files(d)
353
425
  contract_diagnostics = None
354
426
  if not (d/'agent_trace.json').exists() or not (d/'actual_outputs.json').exists():
355
427
  contract_diagnostics = build_contract_diagnostics(d, command=cmd, working_directory=d, agent_display_name='Codex', prompt=prompt)
356
428
  with (d/'final_message.txt').open('a') as f:
357
429
  f.write('\n\n' + diagnostics_text(contract_diagnostics))
358
430
  trace, actual, trace_valid = ensure_attempt_outputs(package_root, spec, attempt_kind, prompt, sandbox, run_error)
431
+ ended_at = utc_now()
432
+ trace = _enrich_runtime_trace(trace, actual, package_root, attempt_kind, 'codex', started_at, ended_at)
359
433
  trace.metadata_json['codex_sandbox']=sandbox
360
434
  trace.metadata_json['codex_command']=[part if part != prompt else '<prompt>' for part in cmd]
361
435
  trace.metadata_json['codex_skip_git_repo_check_supported']=skip_supported
362
436
  trace.metadata_json['codex_skip_git_repo_check_used']='--skip-git-repo-check' in cmd
363
437
  trace.metadata_json['trace_valid']=trace_valid
438
+ if recovered_contract_files:
439
+ trace.metadata_json['contract_files_recovered_from'] = recovered_contract_files
364
440
  if contract_diagnostics:
365
441
  trace.metadata_json['apprentice_agent_contract_diagnostics']=contract_diagnostics
366
442
  write_json(d/'agent_trace.json', trace)
367
443
  if actual.metadata_json is None: actual.metadata_json={}
368
444
  actual.metadata_json['codex_sandbox']=sandbox
445
+ if recovered_contract_files:
446
+ actual.metadata_json['contract_files_recovered_from'] = recovered_contract_files
369
447
  if contract_diagnostics:
370
448
  actual.metadata_json['apprentice_agent_contract_diagnostics']=contract_diagnostics
371
449
  op_error = _apprentice_operational_error(run_error, stdout, stderr, returncode)
@@ -393,6 +471,7 @@ def run_codex_attempt(package_root: Path, raw: RawTaskRecord, spec: TaskIntakeSp
393
471
 
394
472
 
395
473
  def run_custom_attempt(package_root: Path, raw: RawTaskRecord, spec: TaskIntakeSpec, attempt_kind='baseline', timeout: int | None=None) -> AttemptResult:
474
+ started_at = utc_now()
396
475
  settings=get_settings()
397
476
  d=_attempt_dir(package_root, attempt_kind)
398
477
  input_files=_copy_attempt_inputs(package_root, d)
@@ -411,6 +490,8 @@ def run_custom_attempt(package_root: Path, raw: RawTaskRecord, spec: TaskIntakeS
411
490
  with (d/'final_message.txt').open('a') as f:
412
491
  f.write('\n\n' + diagnostics_text(contract_diagnostics))
413
492
  trace, actual, trace_valid=ensure_attempt_outputs(package_root, spec, attempt_kind, prompt, 'custom', err)
493
+ ended_at = utc_now()
494
+ trace = _enrich_runtime_trace(trace, actual, package_root, attempt_kind, 'custom', started_at, ended_at)
414
495
  if actual.metadata_json is None:
415
496
  actual.metadata_json = {}
416
497
  actual.metadata_json['apprentice_agent_contract_diagnostics'] = contract_diagnostics
@@ -449,6 +530,8 @@ def run_custom_attempt(package_root: Path, raw: RawTaskRecord, spec: TaskIntakeS
449
530
  with (d/'final_message.txt').open('a') as f:
450
531
  f.write('\n\n' + diagnostics_text(contract_diagnostics))
451
532
  trace, actual, trace_valid=ensure_attempt_outputs(package_root, spec, attempt_kind, prompt, 'custom', run_error)
533
+ ended_at = utc_now()
534
+ trace = _enrich_runtime_trace(trace, actual, package_root, attempt_kind, 'custom', started_at, ended_at)
452
535
  trace.metadata_json['custom_worker_display_name']=settings.custom_worker_display_name
453
536
  trace.metadata_json['custom_worker_command_template']=template
454
537
  trace.metadata_json['trace_valid']=trace_valid