agent-apprenticeship 0.1.1 → 0.1.2
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.
- package/README.md +6 -15
- package/bin/agent-apprenticeship.js +56 -12
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/agent_apprenticeship_trace/__init__.py +1 -1
- package/src/agent_apprenticeship_trace/apprentice_adapters.py +2 -1
- package/src/agent_apprenticeship_trace/bundle_exporter.py +63 -3
- package/src/agent_apprenticeship_trace/cli.py +366 -53
- package/src/agent_apprenticeship_trace/codex_runner.py +46 -3
- package/src/agent_apprenticeship_trace/config.py +16 -0
- package/src/agent_apprenticeship_trace/openai_structured.py +6 -0
- package/src/agent_apprenticeship_trace/public_run.py +118 -57
- package/src/agent_apprenticeship_trace/task_intake.py +45 -2
|
@@ -2,11 +2,16 @@ from __future__ import annotations
|
|
|
2
2
|
import base64
|
|
3
3
|
import hashlib
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
6
|
+
import select
|
|
5
7
|
import shlex
|
|
6
8
|
import sys
|
|
7
9
|
import subprocess
|
|
8
10
|
import shutil, json
|
|
9
11
|
import zipfile
|
|
12
|
+
import urllib.error
|
|
13
|
+
import urllib.parse
|
|
14
|
+
import urllib.request
|
|
10
15
|
from contextlib import contextmanager
|
|
11
16
|
from datetime import datetime, timezone
|
|
12
17
|
from pathlib import Path
|
|
@@ -32,6 +37,7 @@ from .config import (
|
|
|
32
37
|
configured_model_provider_ready,
|
|
33
38
|
apprentice_agent_readiness_status,
|
|
34
39
|
mentor_model_provider_readiness,
|
|
40
|
+
mentor_mode_display,
|
|
35
41
|
normalize_mentor_mode,
|
|
36
42
|
normalize_ecosystem_auto_share,
|
|
37
43
|
normalize_sensitive_info_masking,
|
|
@@ -61,9 +67,9 @@ from .learning import (
|
|
|
61
67
|
update_pack_status,
|
|
62
68
|
write_before_after_result,
|
|
63
69
|
)
|
|
64
|
-
from .public_run import apprentice_agent_readiness, continue_session, finish_session, run_prompt_task, run_root_for
|
|
65
|
-
from .progress import format_progress_event, format_run_status, read_run_status, watch_progress
|
|
66
|
-
from .recipes import MODEL_PROVIDER_RECIPES,
|
|
70
|
+
from .public_run import RunInterrupted, apprentice_agent_readiness, continue_session, finish_session, run_prompt_task, run_root_for
|
|
71
|
+
from .progress import append_progress_event, format_progress_event, format_run_status, read_run_status, watch_progress
|
|
72
|
+
from .recipes import MODEL_PROVIDER_RECIPES, WORKER_AGENT_RECIPES
|
|
67
73
|
app=typer.Typer(add_completion=False)
|
|
68
74
|
configure_app=typer.Typer(add_completion=False, help='Configure Apprentice Agents and Mentor Model Providers.')
|
|
69
75
|
ecosystem_app=typer.Typer(add_completion=False, help='Explore and contribute Agent Apprenticeship ecosystem bundles.')
|
|
@@ -76,6 +82,11 @@ app.add_typer(learn_app, name='learn')
|
|
|
76
82
|
|
|
77
83
|
SLACK_LINK='https://join.slack.com/t/fsycommunity/shared_invite/zt-37417grrb-jFD6BQIYgC5wEMrW2bHssw'
|
|
78
84
|
|
|
85
|
+
FIRST_RUN_BANNER = """╭────────────────────────────────────────────╮
|
|
86
|
+
│ Agent Apprenticeship │
|
|
87
|
+
│ Real-world work -> reusable agent signals │
|
|
88
|
+
╰────────────────────────────────────────────╯"""
|
|
89
|
+
|
|
79
90
|
MODEL_KEY_CANDIDATES = {
|
|
80
91
|
"openai": ["OPENAI_API_KEY"],
|
|
81
92
|
"anthropic": ["ANTHROPIC_API_KEY"],
|
|
@@ -102,6 +113,11 @@ def _print_public_ecosystem_contribution_help(bundle: str | Path) -> None:
|
|
|
102
113
|
typer.echo('Public ecosystem:')
|
|
103
114
|
typer.echo(_ecosystem_repo_url())
|
|
104
115
|
|
|
116
|
+
|
|
117
|
+
def _print_first_run_banner() -> None:
|
|
118
|
+
typer.echo(FIRST_RUN_BANNER)
|
|
119
|
+
typer.echo("")
|
|
120
|
+
|
|
105
121
|
def _print_task_result(
|
|
106
122
|
status: dict,
|
|
107
123
|
*,
|
|
@@ -202,6 +218,24 @@ def _env_next_step(env_var: str | None) -> str:
|
|
|
202
218
|
return f"Next: export {env}=... or add {env}=... to ~/.agent-apprenticeship/.env.local"
|
|
203
219
|
|
|
204
220
|
|
|
221
|
+
def _stdin_allows_prompt() -> bool:
|
|
222
|
+
if sys.stdin.isatty():
|
|
223
|
+
return True
|
|
224
|
+
try:
|
|
225
|
+
ready, _, _ = select.select([sys.stdin], [], [], 0)
|
|
226
|
+
if not ready:
|
|
227
|
+
return False
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
try:
|
|
231
|
+
pos = sys.stdin.tell()
|
|
232
|
+
char = sys.stdin.read(1)
|
|
233
|
+
sys.stdin.seek(pos)
|
|
234
|
+
return bool(char)
|
|
235
|
+
except Exception:
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
|
|
205
239
|
def _detect_apprentice_agents() -> list[dict]:
|
|
206
240
|
detected = []
|
|
207
241
|
for agent_id, commands in AGENT_COMMAND_CANDIDATES.items():
|
|
@@ -348,7 +382,7 @@ def _progress_callback(quiet: bool=False, verbose: bool=False, json_progress: bo
|
|
|
348
382
|
typer.echo('')
|
|
349
383
|
typer.echo(f'Run: {status.get("run_id")}')
|
|
350
384
|
typer.echo(f'Apprentice Agent: {status.get("apprentice_agent") or status.get("worker_agent")}')
|
|
351
|
-
typer.echo(f'Mentor Mode: {status.get("mentor_mode")}')
|
|
385
|
+
typer.echo(f'Mentor Mode: {mentor_mode_display(status.get("mentor_mode"))}')
|
|
352
386
|
typer.echo(f'Maximum Improvement Loops: {status.get("maximum_improvement_loops")}')
|
|
353
387
|
typer.echo(f'Task workspace: {status.get("task_workspace_path")}')
|
|
354
388
|
typer.echo(f'Artifacts: {status.get("artifacts_path")}')
|
|
@@ -574,6 +608,13 @@ def _ensure_evaluation_ready(settings, interactive: bool=True):
|
|
|
574
608
|
return settings
|
|
575
609
|
if not interactive:
|
|
576
610
|
raise typer.BadParameter('model-assisted/hybrid Mentor Mode requires a ready Mentor Model Provider; use configure model or --mentor-mode expert-led')
|
|
611
|
+
if not _stdin_allows_prompt():
|
|
612
|
+
readiness = mentor_model_provider_readiness(settings)
|
|
613
|
+
reason = readiness.get("reason") or "Mentor Model Provider is not ready."
|
|
614
|
+
raise typer.BadParameter(
|
|
615
|
+
f"model-assisted/hybrid Mentor Mode requires a ready Mentor Model Provider. {reason} "
|
|
616
|
+
"Run `apprentice doctor --live`, `apprentice configure model`, or use `--mentor-mode expert-led`."
|
|
617
|
+
)
|
|
577
618
|
typer.echo('Model-assisted Mentor Mode needs a ready Mentor Model Provider.')
|
|
578
619
|
readiness=mentor_model_provider_readiness(settings)
|
|
579
620
|
if readiness.get("provider"):
|
|
@@ -584,7 +625,14 @@ def _ensure_evaluation_ready(settings, interactive: bool=True):
|
|
|
584
625
|
typer.echo('2. Run this session in expert-led mode')
|
|
585
626
|
typer.echo('3. Cancel')
|
|
586
627
|
typer.echo('')
|
|
587
|
-
|
|
628
|
+
try:
|
|
629
|
+
choice=typer.prompt('Default', default='expert-led').strip().lower()
|
|
630
|
+
except typer.Abort:
|
|
631
|
+
reason = readiness.get("reason") or "Mentor Model Provider is not ready."
|
|
632
|
+
raise typer.BadParameter(
|
|
633
|
+
f"model-assisted/hybrid Mentor Mode requires a ready Mentor Model Provider. {reason} "
|
|
634
|
+
"Run `apprentice doctor --live`, `apprentice configure model`, or use `--mentor-mode expert-led`."
|
|
635
|
+
) from None
|
|
588
636
|
if choice in {'1','setup','set up','configure','configure mentor model provider','set up model provider now'}:
|
|
589
637
|
configured=_configure_model_impl(None, None, None, test_connection=False)
|
|
590
638
|
return configured
|
|
@@ -602,6 +650,14 @@ def _ensure_apprentice_agent_ready(settings, runner: str | None=None, interactiv
|
|
|
602
650
|
reason = status.get("reason")
|
|
603
651
|
if not interactive:
|
|
604
652
|
raise typer.BadParameter(reason or 'Apprentice Agent is not ready.')
|
|
653
|
+
if not _stdin_allows_prompt():
|
|
654
|
+
if status.get("command_found"):
|
|
655
|
+
typer.echo("Apprentice Agent readiness is untested; command was found, continuing without an interactive prompt.")
|
|
656
|
+
return
|
|
657
|
+
raise typer.BadParameter(
|
|
658
|
+
f"Apprentice Agent is not ready. {reason or 'Command was not found.'} "
|
|
659
|
+
"Run `apprentice configure agent` or install/authenticate a selected Apprentice Agent CLI."
|
|
660
|
+
)
|
|
605
661
|
if status.get("status") == "untested":
|
|
606
662
|
typer.echo('Apprentice Agent Status: Untested')
|
|
607
663
|
if reason:
|
|
@@ -613,7 +669,16 @@ def _ensure_apprentice_agent_ready(settings, runner: str | None=None, interactiv
|
|
|
613
669
|
typer.echo('3. Configure Apprentice Agent')
|
|
614
670
|
typer.echo('4. Cancel')
|
|
615
671
|
typer.echo('')
|
|
616
|
-
|
|
672
|
+
try:
|
|
673
|
+
choice=typer.prompt('Default', default='run anyway').strip().lower()
|
|
674
|
+
except typer.Abort:
|
|
675
|
+
if status.get("command_found"):
|
|
676
|
+
typer.echo("Apprentice Agent readiness is untested; command was found, continuing without an interactive prompt.")
|
|
677
|
+
return
|
|
678
|
+
raise typer.BadParameter(
|
|
679
|
+
f"Apprentice Agent is not ready. {reason or 'Command was not found.'} "
|
|
680
|
+
"Run `apprentice configure agent` or install/authenticate a selected Apprentice Agent CLI."
|
|
681
|
+
) from None
|
|
617
682
|
if choice in {'1','run check','check'}:
|
|
618
683
|
if status.get("command_found"):
|
|
619
684
|
update_settings(apprentice_agent_readiness_status="ready", apprentice_agent_readiness_reason="Command availability check passed.")
|
|
@@ -635,7 +700,16 @@ def _ensure_apprentice_agent_ready(settings, runner: str | None=None, interactiv
|
|
|
635
700
|
typer.echo('2. Continue anyway')
|
|
636
701
|
typer.echo('3. Cancel')
|
|
637
702
|
typer.echo('')
|
|
638
|
-
|
|
703
|
+
try:
|
|
704
|
+
choice=typer.prompt('Default', default='cancel').strip().lower()
|
|
705
|
+
except typer.Abort:
|
|
706
|
+
if status.get("command_found"):
|
|
707
|
+
typer.echo("Apprentice Agent is not ready; command was found, continuing without an interactive prompt.")
|
|
708
|
+
return
|
|
709
|
+
raise typer.BadParameter(
|
|
710
|
+
f"Apprentice Agent is not ready. {reason or 'Command was not found.'} "
|
|
711
|
+
"Run `apprentice configure agent` or install/authenticate a selected Apprentice Agent CLI."
|
|
712
|
+
) from None
|
|
639
713
|
if choice in {'1','configure','configure apprentice agent','configure agent'}:
|
|
640
714
|
typer.echo('Run: agent-apprenticeship configure agent')
|
|
641
715
|
raise typer.Exit(1)
|
|
@@ -649,6 +723,7 @@ def init(
|
|
|
649
723
|
setup: bool=typer.Option(False, '--setup', help='Detect installed Apprentice Agents and Mentor Model Provider keys.'),
|
|
650
724
|
defaults: bool=typer.Option(False, '--defaults', '--non-interactive', help='Initialize with deterministic defaults and never prompt.'),
|
|
651
725
|
):
|
|
726
|
+
_print_first_run_banner()
|
|
652
727
|
path=init_settings(overwrite=overwrite)
|
|
653
728
|
typer.echo(f'initialized Agent Apprenticeship settings at {path}')
|
|
654
729
|
if defaults:
|
|
@@ -697,7 +772,7 @@ def doctor(
|
|
|
697
772
|
typer.echo(f'Apprentice Agent command: {agent.get("command") or "Not configured"}')
|
|
698
773
|
typer.echo(f'Apprentice Agent command found: {_yes_no(bool(agent.get("command_found")))}')
|
|
699
774
|
typer.echo(_status_line('Apprentice Agent readiness', agent.get('status'), agent.get('reason')))
|
|
700
|
-
typer.echo(f'Mentor Mode: {settings.mentor_mode}')
|
|
775
|
+
typer.echo(f'Mentor Mode: {mentor_mode_display(settings.mentor_mode)}')
|
|
701
776
|
typer.echo(f'Mentor Model Provider: {provider.get("provider_display")}')
|
|
702
777
|
typer.echo(f'Mentor model: {provider.get("model") or "Not configured"}')
|
|
703
778
|
typer.echo(f'API key env var: {provider.get("api_key_env_var") or "Not configured"}')
|
|
@@ -926,11 +1001,6 @@ def _print_integration_groups(report: dict) -> None:
|
|
|
926
1001
|
typer.echo(f"- {item}")
|
|
927
1002
|
else:
|
|
928
1003
|
typer.echo("- none")
|
|
929
|
-
if REMOVED_V0_MODEL_PROVIDER_IDS:
|
|
930
|
-
typer.echo("")
|
|
931
|
-
typer.echo("Removed From V0")
|
|
932
|
-
for provider_id in REMOVED_V0_MODEL_PROVIDER_IDS:
|
|
933
|
-
typer.echo(f"- {provider_id}")
|
|
934
1004
|
|
|
935
1005
|
|
|
936
1006
|
@app.command('integrations')
|
|
@@ -999,8 +1069,6 @@ def _print_certification_groups(report: dict) -> None:
|
|
|
999
1069
|
typer.echo(f" - {item}")
|
|
1000
1070
|
else:
|
|
1001
1071
|
typer.echo(" - none")
|
|
1002
|
-
if REMOVED_V0_MODEL_PROVIDER_IDS:
|
|
1003
|
-
typer.echo(f"- Removed from v0: {', '.join(REMOVED_V0_MODEL_PROVIDER_IDS)}")
|
|
1004
1072
|
|
|
1005
1073
|
|
|
1006
1074
|
@app.command('certify-integrations')
|
|
@@ -1205,7 +1273,15 @@ def start(
|
|
|
1205
1273
|
hybrid_auto_approve=hybrid_auto_approve,
|
|
1206
1274
|
mentor_interactive_checkpoints=interactive_checkpoints,
|
|
1207
1275
|
):
|
|
1208
|
-
|
|
1276
|
+
try:
|
|
1277
|
+
run_root,bundle=run_prompt_task(instruction, assets=assets, settings=settings, runner=runner, progress_callback=_progress_callback(quiet, verbose, json_progress))
|
|
1278
|
+
except RunInterrupted as exc:
|
|
1279
|
+
if not quiet and not json_progress:
|
|
1280
|
+
typer.echo("")
|
|
1281
|
+
typer.echo("Run interrupted.")
|
|
1282
|
+
typer.echo("Run directory:")
|
|
1283
|
+
typer.echo(str(exc.run_root))
|
|
1284
|
+
raise typer.Exit(130)
|
|
1209
1285
|
status = read_run_status(run_root)
|
|
1210
1286
|
if quiet:
|
|
1211
1287
|
_print_task_result(status, include_contribution_help=False)
|
|
@@ -1263,15 +1339,23 @@ def run_prompt(
|
|
|
1263
1339
|
hybrid_auto_approve=hybrid_auto_approve,
|
|
1264
1340
|
mentor_interactive_checkpoints=interactive_checkpoints,
|
|
1265
1341
|
):
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1342
|
+
try:
|
|
1343
|
+
run_root,bundle=run_prompt_task(
|
|
1344
|
+
effective_instruction,
|
|
1345
|
+
assets=asset,
|
|
1346
|
+
run_id=run_id,
|
|
1347
|
+
settings=settings,
|
|
1348
|
+
runner=runner,
|
|
1349
|
+
progress_callback=_progress_callback(quiet, verbose, json_progress),
|
|
1350
|
+
experience_pack_refs=experience_refs,
|
|
1351
|
+
)
|
|
1352
|
+
except RunInterrupted as exc:
|
|
1353
|
+
if not quiet and not json_progress:
|
|
1354
|
+
typer.echo("")
|
|
1355
|
+
typer.echo("Run interrupted.")
|
|
1356
|
+
typer.echo("Run directory:")
|
|
1357
|
+
typer.echo(str(exc.run_root))
|
|
1358
|
+
raise typer.Exit(130)
|
|
1275
1359
|
status = read_run_status(run_root)
|
|
1276
1360
|
if quiet:
|
|
1277
1361
|
_print_task_result(status, include_contribution_help=False)
|
|
@@ -1324,7 +1408,28 @@ def continue_run(
|
|
|
1324
1408
|
hybrid_auto_approve=hybrid_auto_approve,
|
|
1325
1409
|
mentor_interactive_checkpoints=interactive_checkpoints,
|
|
1326
1410
|
):
|
|
1327
|
-
|
|
1411
|
+
try:
|
|
1412
|
+
run_root,bundle=continue_session(run_id, followup_instruction, assets=asset, run_loop=run_loop, settings=settings, runner=runner, progress_callback=_progress_callback(quiet, verbose, json_progress))
|
|
1413
|
+
except KeyboardInterrupt:
|
|
1414
|
+
previous_status = read_run_status(run_root)
|
|
1415
|
+
append_progress_event(
|
|
1416
|
+
run_root,
|
|
1417
|
+
"followup_interrupted",
|
|
1418
|
+
run_id=run_root.name,
|
|
1419
|
+
message="Follow-up interrupted by user.",
|
|
1420
|
+
phase="interrupted",
|
|
1421
|
+
run_status="partial",
|
|
1422
|
+
task_status="partial",
|
|
1423
|
+
contribution_bundle_path=previous_status.get("contribution_bundle_path"),
|
|
1424
|
+
operational_error="Follow-up interrupted by user.",
|
|
1425
|
+
)
|
|
1426
|
+
if not quiet and not json_progress:
|
|
1427
|
+
typer.echo("")
|
|
1428
|
+
typer.echo("Follow-up interrupted.")
|
|
1429
|
+
if previous_status.get("contribution_bundle_path"):
|
|
1430
|
+
typer.echo("Previous Contribution Bundle:")
|
|
1431
|
+
typer.echo(str(previous_status.get("contribution_bundle_path")))
|
|
1432
|
+
raise typer.Exit(130)
|
|
1328
1433
|
status = read_run_status(run_root)
|
|
1329
1434
|
if quiet:
|
|
1330
1435
|
_print_task_result(status, followup=True, record_only=not run_loop, include_contribution_help=False)
|
|
@@ -1549,6 +1654,150 @@ def _index_from_gh(repo: str) -> list[dict]:
|
|
|
1549
1654
|
)
|
|
1550
1655
|
|
|
1551
1656
|
|
|
1657
|
+
def _read_public_url_text(url: str, *, timeout: int = 20) -> str:
|
|
1658
|
+
req = urllib.request.Request(
|
|
1659
|
+
url,
|
|
1660
|
+
headers={
|
|
1661
|
+
"Accept": "application/vnd.github+json",
|
|
1662
|
+
"User-Agent": "agent-apprenticeship-cli",
|
|
1663
|
+
},
|
|
1664
|
+
)
|
|
1665
|
+
with urllib.request.urlopen(req, timeout=timeout) as response:
|
|
1666
|
+
return response.read().decode("utf-8")
|
|
1667
|
+
|
|
1668
|
+
|
|
1669
|
+
def _github_raw_url(repo: str, path: str, ref: str = "main") -> str:
|
|
1670
|
+
quoted = urllib.parse.quote(path.strip("/"), safe="/")
|
|
1671
|
+
return f"https://raw.githubusercontent.com/{repo}/{ref}/{quoted}"
|
|
1672
|
+
|
|
1673
|
+
|
|
1674
|
+
def _github_contents_url(repo: str, path: str, ref: str = "main") -> str:
|
|
1675
|
+
quoted = urllib.parse.quote(path.strip("/"), safe="/")
|
|
1676
|
+
suffix = f"/{quoted}" if quoted else ""
|
|
1677
|
+
return f"https://api.github.com/repos/{repo}/contents{suffix}?ref={urllib.parse.quote(ref)}"
|
|
1678
|
+
|
|
1679
|
+
|
|
1680
|
+
def _public_index_cache_path(repo: str) -> Path:
|
|
1681
|
+
slug = re.sub(r"[^A-Za-z0-9_.-]+", "_", repo)
|
|
1682
|
+
return get_settings().app_home / "ecosystem" / "public_index_cache" / f"{slug}.json"
|
|
1683
|
+
|
|
1684
|
+
|
|
1685
|
+
def _load_public_index_cache(repo: str) -> list[dict] | None:
|
|
1686
|
+
path = _public_index_cache_path(repo)
|
|
1687
|
+
if not path.exists():
|
|
1688
|
+
return None
|
|
1689
|
+
try:
|
|
1690
|
+
data = read_json(path)
|
|
1691
|
+
except Exception:
|
|
1692
|
+
return None
|
|
1693
|
+
rows = data.get("rows") if isinstance(data, dict) else None
|
|
1694
|
+
if not isinstance(rows, list):
|
|
1695
|
+
return None
|
|
1696
|
+
return [row for row in rows if isinstance(row, dict)]
|
|
1697
|
+
|
|
1698
|
+
|
|
1699
|
+
def _write_public_index_cache(repo: str, rows: list[dict]) -> None:
|
|
1700
|
+
path = _public_index_cache_path(repo)
|
|
1701
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1702
|
+
write_json(
|
|
1703
|
+
path,
|
|
1704
|
+
{
|
|
1705
|
+
"kind": "public_ecosystem_index_cache",
|
|
1706
|
+
"repo": repo,
|
|
1707
|
+
"repo_url": _ecosystem_repo_url(repo),
|
|
1708
|
+
"cached_at": datetime.now(timezone.utc).isoformat(),
|
|
1709
|
+
"rows": rows,
|
|
1710
|
+
},
|
|
1711
|
+
)
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
def _rows_from_index_text(text: str, *, suffix: str) -> list[dict]:
|
|
1715
|
+
if suffix == ".jsonl":
|
|
1716
|
+
return [json.loads(line) for line in text.splitlines() if line.strip()]
|
|
1717
|
+
data = json.loads(text or "{}")
|
|
1718
|
+
if isinstance(data, list):
|
|
1719
|
+
return [row for row in data if isinstance(row, dict)]
|
|
1720
|
+
if isinstance(data, dict):
|
|
1721
|
+
rows = (
|
|
1722
|
+
data.get("bundles")
|
|
1723
|
+
or data.get("items")
|
|
1724
|
+
or data.get("entries")
|
|
1725
|
+
or data.get("contributions")
|
|
1726
|
+
or []
|
|
1727
|
+
)
|
|
1728
|
+
return [row for row in rows if isinstance(row, dict)]
|
|
1729
|
+
return []
|
|
1730
|
+
|
|
1731
|
+
|
|
1732
|
+
def _index_from_public_repo_url(repo: str) -> list[dict]:
|
|
1733
|
+
rows: list[dict] = []
|
|
1734
|
+
errors: list[str] = []
|
|
1735
|
+
for path in [
|
|
1736
|
+
"seed_dataset/ecosystem_registry.jsonl",
|
|
1737
|
+
"seed_dataset/ecosystem_registry.json",
|
|
1738
|
+
"ecosystem/contributions/index.json",
|
|
1739
|
+
"ecosystem/contributions/index.jsonl",
|
|
1740
|
+
]:
|
|
1741
|
+
try:
|
|
1742
|
+
text = _read_public_url_text(_github_raw_url(repo, path), timeout=20)
|
|
1743
|
+
except (urllib.error.URLError, TimeoutError, OSError) as exc:
|
|
1744
|
+
errors.append(f"{path}: {exc}")
|
|
1745
|
+
continue
|
|
1746
|
+
for row in _rows_from_index_text(text, suffix=Path(path).suffix):
|
|
1747
|
+
out = dict(row)
|
|
1748
|
+
out.setdefault("public_repo_slug", repo)
|
|
1749
|
+
out.setdefault("public_repo_url", _ecosystem_repo_url(repo))
|
|
1750
|
+
if path.startswith("seed_dataset/"):
|
|
1751
|
+
out.setdefault("kind", "seed_task")
|
|
1752
|
+
out.setdefault("experience_source_type", "seed_task")
|
|
1753
|
+
else:
|
|
1754
|
+
out.setdefault("kind", "contribution_bundle")
|
|
1755
|
+
out.setdefault("experience_source_type", "contribution_bundle")
|
|
1756
|
+
rows.append(out)
|
|
1757
|
+
if not rows:
|
|
1758
|
+
detail = "; ".join(errors[:3])
|
|
1759
|
+
raise FileNotFoundError(
|
|
1760
|
+
f"Could not read public ecosystem index from https://github.com/{repo}. {detail}".strip()
|
|
1761
|
+
)
|
|
1762
|
+
deduped: list[dict] = []
|
|
1763
|
+
seen: set[str] = set()
|
|
1764
|
+
for row in rows:
|
|
1765
|
+
key = str(row.get("bundle_id") or row.get("seed_task_id") or row.get("task_id") or "")
|
|
1766
|
+
if key and key in seen:
|
|
1767
|
+
continue
|
|
1768
|
+
if key:
|
|
1769
|
+
seen.add(key)
|
|
1770
|
+
deduped.append(row)
|
|
1771
|
+
_write_public_index_cache(repo, deduped)
|
|
1772
|
+
return deduped
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
def _download_public_repo_path(repo: str, rel_path: str, dest: Path) -> None:
|
|
1776
|
+
data = json.loads(_read_public_url_text(_github_contents_url(repo, rel_path), timeout=20))
|
|
1777
|
+
if isinstance(data, list):
|
|
1778
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
1779
|
+
for item in data:
|
|
1780
|
+
item_type = item.get("type")
|
|
1781
|
+
item_path = item.get("path")
|
|
1782
|
+
item_name = item.get("name")
|
|
1783
|
+
if not item_path or not item_name:
|
|
1784
|
+
continue
|
|
1785
|
+
if item_type == "dir":
|
|
1786
|
+
_download_public_repo_path(repo, item_path, dest / item_name)
|
|
1787
|
+
elif item_type == "file":
|
|
1788
|
+
download_url = item.get("download_url") or _github_raw_url(repo, item_path)
|
|
1789
|
+
target = dest / item_name
|
|
1790
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1791
|
+
target.write_text(_read_public_url_text(download_url, timeout=20))
|
|
1792
|
+
return
|
|
1793
|
+
if isinstance(data, dict) and data.get("type") == "file":
|
|
1794
|
+
download_url = data.get("download_url") or _github_raw_url(repo, data.get("path") or rel_path)
|
|
1795
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
1796
|
+
dest.write_text(_read_public_url_text(download_url, timeout=20))
|
|
1797
|
+
return
|
|
1798
|
+
raise FileNotFoundError(f"Could not download public ecosystem path: {rel_path}")
|
|
1799
|
+
|
|
1800
|
+
|
|
1552
1801
|
def _load_json_rows(path: Path) -> list[dict]:
|
|
1553
1802
|
if not path.exists():
|
|
1554
1803
|
return []
|
|
@@ -1657,15 +1906,21 @@ def _load_registry(registry: Path | None=None) -> list[dict]:
|
|
|
1657
1906
|
return _index_from_public_repo_path(repo_path)
|
|
1658
1907
|
repo = _configured_ecosystem_repo()
|
|
1659
1908
|
if repo:
|
|
1909
|
+
cached = _load_public_index_cache(repo)
|
|
1910
|
+
if cached:
|
|
1911
|
+
return cached
|
|
1660
1912
|
try:
|
|
1661
|
-
return
|
|
1913
|
+
return _index_from_public_repo_url(repo)
|
|
1662
1914
|
except FileNotFoundError as exc:
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1915
|
+
try:
|
|
1916
|
+
return _index_from_gh(repo)
|
|
1917
|
+
except FileNotFoundError as gh_exc:
|
|
1918
|
+
seed_rows = search_learning_sources(None)
|
|
1919
|
+
if repo == DEFAULT_PUBLIC_ECOSYSTEM_REPO and seed_rows:
|
|
1920
|
+
return seed_rows
|
|
1921
|
+
raise FileNotFoundError(
|
|
1922
|
+
f"Public ecosystem repo is configured as {repo}, but the index could not be read. {exc} {gh_exc}"
|
|
1923
|
+
)
|
|
1669
1924
|
path=_default_registry_path()
|
|
1670
1925
|
if not path.exists():
|
|
1671
1926
|
seed_rows = search_learning_sources(None)
|
|
@@ -1778,7 +2033,7 @@ def _format_registry_row(row: dict) -> str:
|
|
|
1778
2033
|
str(row.get("task_status") or row.get("run_status") or "status_unknown"),
|
|
1779
2034
|
]
|
|
1780
2035
|
if row.get("mentor_mode"):
|
|
1781
|
-
details.append(f"mentor_mode={row.get('mentor_mode')}")
|
|
2036
|
+
details.append(f"mentor_mode={mentor_mode_display(row.get('mentor_mode'))}")
|
|
1782
2037
|
traces = row.get("traced_steps")
|
|
1783
2038
|
if traces is not None:
|
|
1784
2039
|
details.append(f"traces: {_format_count(traces)}")
|
|
@@ -1893,7 +2148,7 @@ def _submission_summary(metadata: dict, bundle: Path, package_zip: Path | None =
|
|
|
1893
2148
|
f"Task Status: {metadata.get('task_status')}",
|
|
1894
2149
|
f"Run Status: {metadata.get('run_status')}",
|
|
1895
2150
|
f"Apprentice Agent: {metadata.get('apprentice_agent') or 'unknown'}",
|
|
1896
|
-
f"Mentor Mode: {metadata.get('mentor_mode')}",
|
|
2151
|
+
f"Mentor Mode: {mentor_mode_display(metadata.get('mentor_mode'))}",
|
|
1897
2152
|
f"Mentor Model Provider: {metadata.get('mentor_model_provider') or 'none'}",
|
|
1898
2153
|
f"Attempts: {metadata.get('attempts')}",
|
|
1899
2154
|
f"Traced Steps: {metadata.get('traced_steps')}",
|
|
@@ -2054,7 +2309,7 @@ def _print_auto_share_summary(metadata: dict) -> None:
|
|
|
2054
2309
|
typer.echo(f"title: {metadata.get('title')}")
|
|
2055
2310
|
typer.echo(f"task_status: {metadata.get('task_status')}")
|
|
2056
2311
|
typer.echo(f"Apprentice Agent: {metadata.get('apprentice_agent') or 'unknown'}")
|
|
2057
|
-
typer.echo(f"Mentor Mode: {metadata.get('mentor_mode')}")
|
|
2312
|
+
typer.echo(f"Mentor Mode: {mentor_mode_display(metadata.get('mentor_mode'))}")
|
|
2058
2313
|
typer.echo(f"Mentor Model Provider: {metadata.get('mentor_model_provider') or 'none'}")
|
|
2059
2314
|
typer.echo(f"artifact_count: {metadata.get('artifact_count')}")
|
|
2060
2315
|
|
|
@@ -2614,6 +2869,7 @@ def ecosystem_pull(
|
|
|
2614
2869
|
row = {**row, "experience_source_type": row.get("experience_source_type") or "seed_task", "pulled_item_type": "seed_task"}
|
|
2615
2870
|
try:
|
|
2616
2871
|
write_json(dest / "ecosystem_item.json", row)
|
|
2872
|
+
public_repo = row.get("public_repo_slug")
|
|
2617
2873
|
|
|
2618
2874
|
def resolve_seed_path(raw: str | None) -> Path | None:
|
|
2619
2875
|
if not raw:
|
|
@@ -2622,15 +2878,17 @@ def ecosystem_pull(
|
|
|
2622
2878
|
return path if path.is_absolute() else Path.cwd() / path
|
|
2623
2879
|
|
|
2624
2880
|
def copy_seed_path(raw: str | None, target_rel: str) -> None:
|
|
2625
|
-
|
|
2626
|
-
if not source_path or not source_path.exists():
|
|
2881
|
+
if not raw:
|
|
2627
2882
|
return
|
|
2883
|
+
source_path = resolve_seed_path(raw)
|
|
2628
2884
|
target = dest / target_rel
|
|
2629
2885
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
2630
|
-
if source_path.is_dir():
|
|
2886
|
+
if source_path and source_path.exists() and source_path.is_dir():
|
|
2631
2887
|
shutil.copytree(source_path, target, dirs_exist_ok=True)
|
|
2632
|
-
|
|
2888
|
+
elif source_path and source_path.exists():
|
|
2633
2889
|
shutil.copy2(source_path, target)
|
|
2890
|
+
elif public_repo:
|
|
2891
|
+
_download_public_repo_path(str(public_repo), str(raw), target)
|
|
2634
2892
|
|
|
2635
2893
|
copy_seed_path(row.get("task_path") or row.get("task_packet_path"), "task")
|
|
2636
2894
|
copy_seed_path(row.get("rubric_path"), "rubric/rubric.json")
|
|
@@ -2641,8 +2899,8 @@ def ecosystem_pull(
|
|
|
2641
2899
|
copy_seed_path(row.get("learning_signals_path"), "learning_signals")
|
|
2642
2900
|
for idx, trace_path in enumerate(row.get("trace_paths") or [], 1):
|
|
2643
2901
|
trace_source = resolve_seed_path(str(trace_path))
|
|
2644
|
-
|
|
2645
|
-
|
|
2902
|
+
target_name = Path(trace_path).parent.name or f"trace_{idx}"
|
|
2903
|
+
if (trace_source and trace_source.exists()) or public_repo:
|
|
2646
2904
|
copy_seed_path(str(trace_path), f"traces/{target_name}/{Path(trace_path).name}")
|
|
2647
2905
|
except OSError as exc:
|
|
2648
2906
|
typer.echo('Could not pull ecosystem seed task.')
|
|
@@ -2677,7 +2935,13 @@ def _summarize_bundle_manifest(manifest: dict) -> str:
|
|
|
2677
2935
|
'agent_apprentice_role','expected_economic_value',
|
|
2678
2936
|
'expected_economic_value_for_agent_apprentice','mentor_mode','task_status','run_status'
|
|
2679
2937
|
]
|
|
2680
|
-
|
|
2938
|
+
lines = []
|
|
2939
|
+
for key in keys:
|
|
2940
|
+
if key not in manifest:
|
|
2941
|
+
continue
|
|
2942
|
+
value = mentor_mode_display(manifest.get(key)) if key == "mentor_mode" else manifest.get(key)
|
|
2943
|
+
lines.append(f"{key}: {value}")
|
|
2944
|
+
return "\n".join(lines)
|
|
2681
2945
|
|
|
2682
2946
|
|
|
2683
2947
|
def _summarize_seed_registry_row(row: dict) -> str:
|
|
@@ -2727,7 +2991,8 @@ def _summarize_contribution_registry_row(row: dict) -> str:
|
|
|
2727
2991
|
"expected_economic_value_for_agent_apprentice",
|
|
2728
2992
|
]:
|
|
2729
2993
|
if row.get(key) is not None:
|
|
2730
|
-
|
|
2994
|
+
value = mentor_mode_display(row.get(key)) if key == "mentor_mode" else row.get(key)
|
|
2995
|
+
lines.append(f"{key}: {value}")
|
|
2731
2996
|
if row.get("domains"):
|
|
2732
2997
|
lines.append(f"domains: {row.get('domains')}")
|
|
2733
2998
|
if row.get("subdomains"):
|
|
@@ -2784,6 +3049,60 @@ def bundle_inspect(path: Path=typer.Argument(..., help='Contribution Bundle fold
|
|
|
2784
3049
|
typer.echo(f"Next: apprentice ecosystem contribute {path}")
|
|
2785
3050
|
|
|
2786
3051
|
|
|
3052
|
+
@bundle_app.command('check')
|
|
3053
|
+
def bundle_check(path: Path=typer.Argument(..., help='Contribution Bundle folder.')):
|
|
3054
|
+
issues: list[str] = []
|
|
3055
|
+
bundle = Path(path).expanduser()
|
|
3056
|
+
manifest_path = bundle / "contribution_manifest.json"
|
|
3057
|
+
if not bundle.exists():
|
|
3058
|
+
issues.append(f"Bundle path does not exist: {bundle}")
|
|
3059
|
+
elif not manifest_path.exists():
|
|
3060
|
+
issues.append(f"Contribution Bundle manifest not found: {manifest_path}")
|
|
3061
|
+
manifest_data = {}
|
|
3062
|
+
if manifest_path.exists():
|
|
3063
|
+
try:
|
|
3064
|
+
manifest_data = read_json(manifest_path)
|
|
3065
|
+
except Exception as exc:
|
|
3066
|
+
issues.append(f"contribution_manifest.json could not be parsed: {exc}")
|
|
3067
|
+
artifact_index_path = bundle / "outputs" / "artifacts_index.json"
|
|
3068
|
+
artifacts: list[dict] = []
|
|
3069
|
+
if artifact_index_path.exists():
|
|
3070
|
+
try:
|
|
3071
|
+
raw_artifacts = read_json(artifact_index_path)
|
|
3072
|
+
if isinstance(raw_artifacts, list):
|
|
3073
|
+
artifacts = raw_artifacts
|
|
3074
|
+
else:
|
|
3075
|
+
issues.append("outputs/artifacts_index.json must contain a JSON list.")
|
|
3076
|
+
except Exception as exc:
|
|
3077
|
+
issues.append(f"outputs/artifacts_index.json could not be parsed: {exc}")
|
|
3078
|
+
for row in artifacts:
|
|
3079
|
+
ref = row.get("package_relative_path") or row.get("artifact_ref")
|
|
3080
|
+
if ref and not (bundle / str(ref)).exists():
|
|
3081
|
+
issues.append(f"Referenced artifact is missing: {ref}")
|
|
3082
|
+
for optional in ["session_events.jsonl", "progress_events.jsonl"]:
|
|
3083
|
+
file_path = bundle / optional
|
|
3084
|
+
if file_path.exists():
|
|
3085
|
+
try:
|
|
3086
|
+
read_jsonl(file_path)
|
|
3087
|
+
except Exception as exc:
|
|
3088
|
+
issues.append(f"{optional} could not be parsed: {exc}")
|
|
3089
|
+
secret_hits = _bundle_secret_hits(bundle) if bundle.exists() else []
|
|
3090
|
+
if secret_hits:
|
|
3091
|
+
issues.append("Obvious secret-like values were found: " + ", ".join(secret_hits[:10]))
|
|
3092
|
+
if issues:
|
|
3093
|
+
typer.echo("Bundle check: needs attention")
|
|
3094
|
+
typer.echo(f"Bundle: {bundle}")
|
|
3095
|
+
for issue in issues:
|
|
3096
|
+
typer.echo(f"Reason: {redact_secrets(issue)}")
|
|
3097
|
+
typer.echo("Next action: fix the packaging issue, then rerun `apprentice bundle check`.")
|
|
3098
|
+
raise typer.Exit(1)
|
|
3099
|
+
status = manifest_data.get("task_status") or manifest_data.get("run_status") or "unknown"
|
|
3100
|
+
typer.echo("Bundle check: passed")
|
|
3101
|
+
typer.echo(f"Bundle: {bundle}")
|
|
3102
|
+
typer.echo(f"Task status: {status}")
|
|
3103
|
+
typer.echo("This bundle is ready to add to the public ecosystem.")
|
|
3104
|
+
|
|
3105
|
+
|
|
2787
3106
|
@bundle_app.command('contribute')
|
|
2788
3107
|
def bundle_contribute(path: Path=typer.Argument(..., help='Contribution Bundle folder.')):
|
|
2789
3108
|
manifest=path/'contribution_manifest.json'
|
|
@@ -2791,11 +3110,10 @@ def bundle_contribute(path: Path=typer.Argument(..., help='Contribution Bundle f
|
|
|
2791
3110
|
typer.echo(f'Contribution Bundle manifest not found: {manifest}')
|
|
2792
3111
|
raise typer.Exit(1)
|
|
2793
3112
|
try:
|
|
2794
|
-
|
|
3113
|
+
submission_dir, package_zip, metadata = _create_ecosystem_submission(path)
|
|
2795
3114
|
except Exception as exc:
|
|
2796
3115
|
typer.echo(f"Could not prepare public ecosystem contribution: {redact_secrets(str(exc))}")
|
|
2797
3116
|
raise typer.Exit(1)
|
|
2798
|
-
metadata = result["metadata"]
|
|
2799
3117
|
typer.echo('Contribution Bundle ready.')
|
|
2800
3118
|
typer.echo('')
|
|
2801
3119
|
typer.echo(f"Bundle ID: {metadata.get('bundle_id')}")
|
|
@@ -2803,14 +3121,9 @@ def bundle_contribute(path: Path=typer.Argument(..., help='Contribution Bundle f
|
|
|
2803
3121
|
typer.echo('Bundle:')
|
|
2804
3122
|
typer.echo(str(path))
|
|
2805
3123
|
typer.echo('')
|
|
2806
|
-
if result["contribution_created"]:
|
|
2807
|
-
typer.echo(f"Public contribution URL: {result['contribution_url']}")
|
|
2808
|
-
typer.echo("No bundle files were uploaded automatically. Attach or submit the generated package according to the repo instructions.")
|
|
2809
|
-
return
|
|
2810
3124
|
typer.echo('No upload was performed by this command.')
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
typer.echo(f"Reason: {reason}")
|
|
3125
|
+
typer.echo(f"Submission package: {package_zip}")
|
|
3126
|
+
typer.echo(f"Submission metadata: {submission_dir / 'ecosystem_submission.json'}")
|
|
2814
3127
|
typer.echo('')
|
|
2815
3128
|
typer.echo('Contribute to public ecosystem:')
|
|
2816
3129
|
typer.echo(f'apprentice ecosystem contribute {path}')
|