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
@@ -43,6 +43,33 @@ def _attempt_summary(pkg: Path | None) -> dict[str, Any]:
43
43
  return {"attempts": len(attempts), "traced_steps": traced_steps, "artifacts": artifacts[:50]}
44
44
 
45
45
 
46
+ def _mark_source_artifact_audited(pkg: Path | None, *, source: str, mode: str, input_source: str, stages: set[str]) -> None:
47
+ if not pkg:
48
+ return
49
+ updates = {
50
+ "mentor_audited": True,
51
+ "mentor_audit_source": source,
52
+ "audit_status": f"{mode}_audit_complete",
53
+ "checkpoint_input_source": input_source,
54
+ }
55
+ targets: list[Path] = []
56
+ if "task_intake" in stages:
57
+ targets.append(pkg / "task" / "task_intake_spec.json")
58
+ if "rubric" in stages:
59
+ targets.append(pkg / "rubric" / "rubric.json")
60
+ for path in targets:
61
+ if not path.exists():
62
+ continue
63
+ try:
64
+ data = read_json(path)
65
+ md = dict(data.get("metadata_json") or {})
66
+ md.update(updates)
67
+ data["metadata_json"] = md
68
+ write_json(path, data)
69
+ except Exception:
70
+ continue
71
+
72
+
46
73
  def _truthy_env(name: str) -> bool:
47
74
  return os.getenv(name, "").strip().lower() in {"1", "true", "yes", "on"}
48
75
 
@@ -81,7 +108,7 @@ def _parse_score(value: str, default: float) -> float:
81
108
  return max(0.0, min(1.0, score))
82
109
 
83
110
 
84
- APPROVE_WORDS = {"accept", "approve", "approved", "yes", "y", "ok", "okay"}
111
+ APPROVE_WORDS = {"accept", "approve", "approved", "confirm", "confirmed", "yes", "y", "ok", "okay"}
85
112
 
86
113
 
87
114
  def _is_approve(value: str) -> bool:
@@ -103,42 +130,42 @@ def _collect_interactive_inputs(
103
130
  summary: dict[str, Any],
104
131
  stages: set[str],
105
132
  ) -> dict[str, Any]:
106
- label = "Expert-led" if mode == "expert_led" else "Hybrid"
133
+ label = "Expert-led"
107
134
  default_passed = "pass" if status.get("task_status") == "completed" else "fail"
108
135
  default_score = "1.0" if default_passed == "pass" else "0.0"
109
136
  collected: dict[str, Any] = {}
110
137
  if "task_intake" in stages:
111
138
  print(f"{label} checkpoint: task intake")
112
- intake_decision = _prompt("Approve or edit task intake", "approve")
139
+ intake_decision = _prompt("Confirm or edit task intake", "confirm")
113
140
  if _is_edit(intake_decision):
114
141
  edited_title = _prompt("Edited title, optional", "")
115
142
  intake_notes = _prompt("Task intake notes, optional", "")
116
143
  decision = "edited"
117
144
  else:
118
145
  edited_title = ""
119
- intake_notes = "Approved as-is." if _is_approve(intake_decision) else f"Approved as-is. Input: {intake_decision}"
120
- decision = "approved"
146
+ intake_notes = "Confirmed as-is." if _is_approve(intake_decision) else f"Confirmed as-is. Input: {intake_decision}"
147
+ decision = "confirmed"
121
148
  collected["task_intake"] = {
122
149
  "decision": decision,
123
150
  "edited_title": edited_title or None,
124
- "notes": intake_notes or "Approved as-is.",
151
+ "notes": intake_notes or "Confirmed as-is.",
125
152
  }
126
153
 
127
154
  if "rubric" in stages:
128
155
  print(f"{label} checkpoint: rubric")
129
- rubric_decision = _prompt("Approve or edit rubric", "approve")
156
+ rubric_decision = _prompt("Confirm or edit rubric", "confirm")
130
157
  if _is_edit(rubric_decision):
131
158
  additional_criterion = _prompt("Add rubric criterion, optional", "")
132
159
  rubric_notes = _prompt("Rubric notes, optional", "")
133
160
  rubric_choice = "edited"
134
161
  else:
135
162
  additional_criterion = ""
136
- rubric_notes = "Rubric approved." if _is_approve(rubric_decision) else f"Rubric approved. Input: {rubric_decision}"
137
- rubric_choice = "approved"
163
+ rubric_notes = "Rubric confirmed." if _is_approve(rubric_decision) else f"Rubric confirmed. Input: {rubric_decision}"
164
+ rubric_choice = "confirmed"
138
165
  collected["rubric"] = {
139
166
  "decision": rubric_choice,
140
167
  "additional_criteria": [additional_criterion] if additional_criterion else [],
141
- "notes": rubric_notes or "Rubric approved.",
168
+ "notes": rubric_notes or "Rubric confirmed.",
142
169
  }
143
170
 
144
171
  if "evaluation" in stages:
@@ -168,12 +195,12 @@ def _collect_interactive_inputs(
168
195
 
169
196
  if "final_approval" in stages:
170
197
  print(f"{label} checkpoint: final bundle approval")
171
- final_decision = _prompt("Approve final Contribution Bundle or hold", "approve").strip().lower()
198
+ final_decision = _prompt("Confirm final Experience Compilation or hold", "confirm").strip().lower()
172
199
  final_notes = _prompt("Final approval notes, optional", "")
173
200
  collected["final_approval"] = {
174
201
  "approved_for_local_bundle": _is_approve(final_decision) or final_decision.startswith("approve"),
175
- "decision": "approved" if (_is_approve(final_decision) or final_decision.startswith("approve")) else "held",
176
- "notes": final_notes or "Final Contribution Bundle checkpoint completed.",
202
+ "decision": "confirmed" if (_is_approve(final_decision) or final_decision.startswith("approve") or final_decision.startswith("confirm")) else "held",
203
+ "notes": final_notes or "Final Experience Compilation checkpoint completed.",
177
204
  }
178
205
 
179
206
  return collected
@@ -273,8 +300,8 @@ def write_mentor_checkpoints(
273
300
  "checkpoint_type": "task_intake",
274
301
  "title": intake_input.get("edited_title") or status.get("task_title"),
275
302
  "instruction": session.get("task_instruction"),
276
- "decision": intake_input.get("decision") or "approved",
277
- "notes": intake_input.get("notes") or ("Approved as-is." if auto_approve else "Human expert checkpoint recorded."),
303
+ "decision": intake_input.get("decision") or "confirmed",
304
+ "notes": intake_input.get("notes") or ("Confirmed as-is." if auto_approve else "Human expert task-intake audit recorded."),
278
305
  "checkpoint_input_source": input_source,
279
306
  },
280
307
  preserve_interactive=preserve_interactive,
@@ -290,9 +317,9 @@ def write_mentor_checkpoints(
290
317
  "rubric/worker_visible_rubric.md",
291
318
  "rubric/rubric.json",
292
319
  ],
293
- "decision": rubric_input.get("decision") or "approved",
320
+ "decision": rubric_input.get("decision") or "confirmed",
294
321
  "additional_criteria": rubric_input.get("additional_criteria") or [],
295
- "notes": rubric_input.get("notes") or ("Default/model-drafted rubric approved." if auto_approve else "Human expert rubric checkpoint recorded."),
322
+ "notes": rubric_input.get("notes") or ("Apprentice-generated rubric confirmed." if auto_approve else "Human expert rubric audit recorded."),
296
323
  "checkpoint_input_source": input_source,
297
324
  },
298
325
  preserve_interactive=preserve_interactive,
@@ -340,15 +367,16 @@ def write_mentor_checkpoints(
340
367
  "checkpoint_type": "final_approval",
341
368
  "approved_for_local_bundle": final_input.get("approved_for_local_bundle", True),
342
369
  "contribution_bundle_path": status.get("contribution_bundle_path"),
343
- "decision": final_input.get("decision") or "approved",
344
- "notes": final_input.get("notes") or "Final Contribution Bundle checkpoint recorded.",
370
+ "decision": final_input.get("decision") or "confirmed",
371
+ "notes": final_input.get("notes") or "Final Experience Compilation checkpoint recorded.",
345
372
  "checkpoint_input_source": input_source,
346
373
  },
347
374
  preserve_interactive=preserve_interactive,
348
375
  )
349
376
  (root / "README.md").write_text(
350
377
  "# Mentor Checkpoints\n\n"
351
- "These files record expert-led or hybrid human approval checkpoints for the apprenticeship session. "
352
- "They are local review artifacts and do not contain raw provider secrets.\n"
378
+ "These files record expert-led or hybrid human audit checkpoints for the apprenticeship session. "
379
+ "They are local audit artifacts and do not contain raw provider secrets.\n"
353
380
  )
381
+ _mark_source_artifact_audited(pkg, source=source, mode=mode, input_source=input_source, stages=enabled_stages)
354
382
  return root
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
- import importlib, json, os, re, sys, time, urllib.error, urllib.parse, urllib.request
2
+ import importlib, json, os, re, signal, sys, threading, time, urllib.error, urllib.parse, urllib.request
3
3
  from pathlib import Path
4
4
  from typing import Any
5
5
  from pydantic import BaseModel
@@ -150,6 +150,69 @@ def _provider_max_output_tokens() -> int:
150
150
  value = 2048
151
151
  return max(256, min(value, 4096))
152
152
 
153
+
154
+ def _provider_request_timeout_seconds() -> int:
155
+ settings = get_settings()
156
+ raw = os.getenv("AA_ROLE_TIMEOUT_SECONDS") or os.getenv("AA_MODEL_ROLE_TIMEOUT_SECONDS")
157
+ if raw:
158
+ try:
159
+ return max(10, min(int(raw), settings.task_timeout_seconds))
160
+ except ValueError:
161
+ pass
162
+ return max(10, min(30, settings.task_timeout_seconds))
163
+
164
+
165
+ def _provider_request_retry_count() -> int:
166
+ raw = os.getenv("AA_ROLE_RETRY_COUNT") or os.getenv("AA_MODEL_ROLE_RETRY_COUNT")
167
+ if raw:
168
+ try:
169
+ return max(1, min(int(raw), 3))
170
+ except ValueError:
171
+ pass
172
+ return 1
173
+
174
+
175
+ def _provider_wall_timeout_seconds() -> int:
176
+ settings = get_settings()
177
+ raw = os.getenv("AA_ROLE_WALL_TIMEOUT_SECONDS") or os.getenv("AA_MODEL_ROLE_WALL_TIMEOUT_SECONDS")
178
+ if raw:
179
+ try:
180
+ return max(1, min(int(raw), settings.task_timeout_seconds))
181
+ except ValueError:
182
+ pass
183
+ return max(12, min(_provider_request_timeout_seconds() + 2, settings.task_timeout_seconds))
184
+
185
+
186
+ def _with_wall_timeout(label: str, fn):
187
+ timeout = _provider_wall_timeout_seconds()
188
+ if threading.current_thread() is not threading.main_thread() or not hasattr(signal, "setitimer"):
189
+ return fn()
190
+ old_handler = signal.getsignal(signal.SIGALRM)
191
+ old_timer = signal.setitimer(signal.ITIMER_REAL, 0)
192
+
193
+ def _raise_timeout(_signum, _frame):
194
+ raise TimeoutError(f"{label} timed out after {timeout} seconds.")
195
+
196
+ try:
197
+ signal.signal(signal.SIGALRM, _raise_timeout)
198
+ signal.setitimer(signal.ITIMER_REAL, timeout)
199
+ return fn()
200
+ finally:
201
+ signal.setitimer(signal.ITIMER_REAL, 0)
202
+ signal.signal(signal.SIGALRM, old_handler)
203
+ if old_timer and old_timer[0] > 0:
204
+ signal.setitimer(signal.ITIMER_REAL, old_timer[0], old_timer[1])
205
+
206
+
207
+ def _construct_openai_client(mod: Any, kwargs: dict[str, Any]) -> Any:
208
+ try:
209
+ return mod.OpenAI(**kwargs)
210
+ except TypeError as exc:
211
+ if "timeout" not in kwargs or "timeout" not in str(exc):
212
+ raise
213
+ return mod.OpenAI(**{k: v for k, v in kwargs.items() if k != "timeout"})
214
+
215
+
153
216
  def get_model_provider_status(provider_id: str | None = None) -> dict[str, Any]:
154
217
  provider_id = _configured_provider(provider_id)
155
218
  settings = get_settings()
@@ -182,7 +245,7 @@ def get_model_provider_status(provider_id: str | None = None) -> dict[str, Any]:
182
245
  kwargs = {"api_key": key}
183
246
  if provider_id in OPENAI_COMPATIBLE_BASE_URLS:
184
247
  kwargs["base_url"] = OPENAI_COMPATIBLE_BASE_URLS[provider_id]
185
- mod.OpenAI(**kwargs)
248
+ _construct_openai_client(mod, kwargs)
186
249
  status["client_constructed_ok"] = True
187
250
  status["provider_available"] = True
188
251
  except Exception as exc:
@@ -232,7 +295,7 @@ def _openai_compatible_response(provider_id: str, model_name: str, prompt: str)
232
295
  settings = get_settings()
233
296
  _env_var, key = _provider_key(provider_id, settings)
234
297
  mod = import_openai_sdk()
235
- kwargs = {"api_key": key}
298
+ kwargs = {"api_key": key, "timeout": _provider_request_timeout_seconds()}
236
299
  if provider_id in OPENAI_COMPATIBLE_BASE_URLS:
237
300
  kwargs["base_url"] = OPENAI_COMPATIBLE_BASE_URLS[provider_id]
238
301
  if provider_id == "openrouter":
@@ -243,7 +306,7 @@ def _openai_compatible_response(provider_id: str, model_name: str, prompt: str)
243
306
  }
244
307
  except Exception:
245
308
  pass
246
- client = mod.OpenAI(**kwargs)
309
+ client = _construct_openai_client(mod, kwargs)
247
310
  if provider_id == "openai":
248
311
  return _response_text(_responses_create_with_retry(client, model_name, prompt))
249
312
  response = client.chat.completions.create(
@@ -267,7 +330,7 @@ def _anthropic_response(model_name: str, prompt: str) -> str:
267
330
  "https://api.anthropic.com/v1/messages",
268
331
  {"x-api-key": key or "", "anthropic-version": "2023-06-01"},
269
332
  payload,
270
- timeout=min(120, settings.task_timeout_seconds),
333
+ timeout=_provider_request_timeout_seconds(),
271
334
  )
272
335
  parts = data.get("content") or []
273
336
  text = "".join(str(part.get("text") or "") for part in parts if isinstance(part, dict))
@@ -284,7 +347,7 @@ def _google_response(model_name: str, prompt: str) -> str:
284
347
  "contents": [{"parts": [{"text": prompt}]}],
285
348
  "generationConfig": {"responseMimeType": "application/json", "maxOutputTokens": _provider_max_output_tokens()},
286
349
  }
287
- data = _http_json(url, {}, payload, timeout=min(120, settings.task_timeout_seconds))
350
+ data = _http_json(url, {}, payload, timeout=_provider_request_timeout_seconds())
288
351
  try:
289
352
  return str(data["candidates"][0]["content"]["parts"][0]["text"])
290
353
  except Exception as exc:
@@ -301,13 +364,14 @@ def _provider_completion_text(provider_id: str, model_name: str, prompt: str) ->
301
364
 
302
365
  def _provider_completion_text_with_retry(provider_id: str, model_name: str, prompt: str) -> str:
303
366
  last: BaseException | None = None
304
- for i in range(2):
367
+ attempts = _provider_request_retry_count()
368
+ for i in range(attempts):
305
369
  try:
306
370
  return _provider_completion_text(provider_id, model_name, prompt)
307
371
  except Exception as exc:
308
372
  last = exc
309
373
  kind, _msg = classify_model_provider_error(exc)
310
- if i > 0 or kind in {"auth_error", "quota_or_credit_error", "bad_model"}:
374
+ if i >= attempts - 1 or kind in {"auth_error", "quota_or_credit_error", "bad_model"}:
311
375
  raise
312
376
  time.sleep(0.75)
313
377
  raise last or RuntimeError("Mentor Model Provider request failed.")
@@ -402,7 +466,7 @@ def _transient_provider_error(exc: BaseException) -> bool:
402
466
  needles=['timeout','rate','temporar','connection reset','disconnect','websocket','service unavailable','gateway','overloaded']
403
467
  return any(n in name or n in msg for n in needles)
404
468
 
405
- def _responses_create_with_retry(client: Any, model_name: str, prompt: str, attempts: int = 3):
469
+ def _responses_create_with_retry(client: Any, model_name: str, prompt: str, attempts: int = 1):
406
470
  last=None
407
471
  for i in range(attempts):
408
472
  try:
@@ -453,7 +517,10 @@ def run_structured_role(role: str, prompt: str, output_model: type[BaseModel], o
453
517
  raise RuntimeError(result.error_message)
454
518
  return result
455
519
  try:
456
- raw=_provider_completion_text_with_retry(provider_id, model_name, prompt)
520
+ raw=_with_wall_timeout(
521
+ f"{status.get('provider_display') or provider_id} {role}",
522
+ lambda: _provider_completion_text_with_retry(provider_id, model_name, prompt),
523
+ )
457
524
  (out_dir/'raw_output.txt').write_text(redact_secrets(raw))
458
525
  try:
459
526
  normalized,json_meta,normalized_used=_validate_role_output(role, raw, output_model, normalizer_context, out_dir)
@@ -464,7 +531,10 @@ def run_structured_role(role: str, prompt: str, output_model: type[BaseModel], o
464
531
  retry_raw=None
465
532
  try:
466
533
  retry_prompt=_strict_json_retry_prompt(role, prompt, first_error)
467
- retry_raw=_provider_completion_text_with_retry(provider_id, model_name, retry_prompt)
534
+ retry_raw=_with_wall_timeout(
535
+ f"{status.get('provider_display') or provider_id} {role} structured retry",
536
+ lambda: _provider_completion_text_with_retry(provider_id, model_name, retry_prompt),
537
+ )
468
538
  (out_dir/'raw_output.retry.txt').write_text(redact_secrets(retry_raw))
469
539
  normalized,json_meta,normalized_used=_validate_role_output(role, retry_raw, output_model, normalizer_context, out_dir)
470
540
  write_json(out_dir/'parsed_output.json', normalized)
@@ -520,7 +590,7 @@ def smoke_prompts() -> dict[str, tuple[str, type[BaseModel]]]:
520
590
  "pass_threshold": 0.7,
521
591
  "observable_evidence": ["artifacts/hello.txt"],
522
592
  "required_artifacts": ["hello.txt"],
523
- "scoring_method": "deterministic",
593
+ "scoring_method": "hybrid",
524
594
  "worker_visible": True,
525
595
  "verifier_only": False,
526
596
  "hidden_reference_required": False,
@@ -613,8 +683,8 @@ def smoke_prompts() -> dict[str, tuple[str, type[BaseModel]]]:
613
683
  "required_artifacts": ["hello.txt"],
614
684
  "disqualifying_errors": [],
615
685
  "partial_credit_allowed": True,
616
- "grader_kind": "deterministic",
617
- "rubric_generation_source": "deterministic_seed",
686
+ "grader_kind": "hybrid",
687
+ "rubric_generation_source": "agent_assisted",
618
688
  "rubric_generation_agent_provider": None,
619
689
  "rubric_generation_agent_model": None,
620
690
  "rubric_generation_confidence": 0.9,
@@ -7,7 +7,7 @@ from .rubric_generation import worker_visible_markdown
7
7
  from .artifact_resolver import artifact_ref_candidates, normalize_artifact_ref
8
8
 
9
9
 
10
- SOURCE_FIELD_KEYS={'source_url_or_ref','source_kind','source_url','source_ref','source_license'}
10
+ SOURCE_FIELD_KEYS={'source_url_or_ref','source_kind','source_url','source_ref','source_basis','source_license'}
11
11
  IGNORED_RELEASE_DIR_NAMES={
12
12
  'node_modules','.venv','__pycache__','.pytest_cache','.mypy_cache','.ruff_cache','.git','.cache','dist','build'
13
13
  }
@@ -52,6 +52,7 @@ def _base_status(run_id: str, run_root: Path, **kwargs: Any) -> dict[str, Any]:
52
52
  "maximum_improvement_loops": kwargs.get("maximum_improvement_loops"),
53
53
  "latest_message": kwargs.get("latest_message") or "Run starting.",
54
54
  "apprentice_agent": kwargs.get("apprentice_agent") or kwargs.get("worker_agent"),
55
+ "apprenticeship_mode": kwargs.get("apprenticeship_mode"),
55
56
  "mentor_mode": kwargs.get("mentor_mode"),
56
57
  "task_title": kwargs.get("task_title"),
57
58
  "task_workspace_path": str(kwargs.get("task_workspace_path") or run_root),
@@ -191,7 +192,7 @@ def format_run_status(status: dict[str, Any]) -> str:
191
192
  f"Artifacts: {status.get('artifacts_path')}",
192
193
  ]
193
194
  if status.get("contribution_bundle_path"):
194
- lines.append(f"Contribution Bundle: {status.get('contribution_bundle_path')}")
195
+ lines.append(f"Experience Compilation: {status.get('contribution_bundle_path')}")
195
196
  if status.get("traced_steps") is not None:
196
197
  lines.append(f"Traced Steps: {status.get('traced_steps')}")
197
198
  if status.get("artifact_count") is not None: