agent-apprenticeship 0.1.6 → 0.1.8
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/bin/agent-apprenticeship.js +85 -37
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/agent_apprenticeship_trace/__init__.py +1 -1
- package/src/agent_apprenticeship_trace/actual_outputs_normalizer.py +104 -28
- package/src/agent_apprenticeship_trace/apprentice_adapters.py +3 -1
- package/src/agent_apprenticeship_trace/artifact_resolver.py +6 -0
- package/src/agent_apprenticeship_trace/bundle_exporter.py +132 -2
- package/src/agent_apprenticeship_trace/cli.py +1171 -262
- package/src/agent_apprenticeship_trace/codex_runner.py +89 -6
- package/src/agent_apprenticeship_trace/config.py +251 -32
- package/src/agent_apprenticeship_trace/ecosystem_r2.py +515 -0
- package/src/agent_apprenticeship_trace/env.py +5 -5
- package/src/agent_apprenticeship_trace/evaluator.py +127 -11
- package/src/agent_apprenticeship_trace/grader.py +12 -11
- package/src/agent_apprenticeship_trace/learning.py +270 -1
- package/src/agent_apprenticeship_trace/llm_output_normalizer.py +8 -7
- package/src/agent_apprenticeship_trace/loop.py +103 -4
- package/src/agent_apprenticeship_trace/loop_review.py +686 -0
- package/src/agent_apprenticeship_trace/mentor_checkpoints.py +49 -21
- package/src/agent_apprenticeship_trace/openai_structured.py +84 -14
- package/src/agent_apprenticeship_trace/package_exporter.py +1 -1
- package/src/agent_apprenticeship_trace/progress.py +2 -1
- package/src/agent_apprenticeship_trace/public_run.py +369 -57
- package/src/agent_apprenticeship_trace/public_sanitizer.py +37 -1
- package/src/agent_apprenticeship_trace/rubric_generation.py +11 -10
- package/src/agent_apprenticeship_trace/schemas.py +378 -3
- package/src/agent_apprenticeship_trace/task_intake.py +43 -9
- package/src/agent_apprenticeship_trace/tdo.py +2304 -0
- package/src/agent_apprenticeship_trace/trace_normalizer.py +351 -17
- package/src/agent_apprenticeship_trace/trace_prompt.py +38 -11
- package/src/agent_apprenticeship_trace/training_signals.py +1 -1
- package/src/agent_apprenticeship_trace/verifier.py +12 -6
|
@@ -24,12 +24,21 @@ from .bundle_exporter import create_contribution_bundle
|
|
|
24
24
|
from .release_exporter import create_release as create_release_impl
|
|
25
25
|
from .validation import validate_release as validate_release_impl, format_counters
|
|
26
26
|
from .config import (
|
|
27
|
+
APPRENTICESHIP_MODES,
|
|
27
28
|
DATA_SHARING_LEVELS,
|
|
29
|
+
DEFAULT_ECOSYSTEM_STORAGE_BACKEND,
|
|
28
30
|
DEFAULT_PUBLIC_ECOSYSTEM_REPO,
|
|
31
|
+
DEFAULT_R2_BUCKET,
|
|
32
|
+
DEFAULT_R2_CONTRIBUTION_PREFIX,
|
|
33
|
+
DEFAULT_R2_INDEX_PREFIX,
|
|
34
|
+
DEFAULT_R2_SEED_PREFIX,
|
|
29
35
|
ECOSYSTEM_AUTO_SHARE_MODES,
|
|
30
36
|
EVALUATION_MODES,
|
|
31
37
|
MENTOR_MODES,
|
|
32
38
|
SENSITIVE_INFO_MASKING_LEVELS,
|
|
39
|
+
TRAINING_CONTRIBUTION_MODES,
|
|
40
|
+
apprenticeship_mode_display,
|
|
41
|
+
apprenticeship_mode_to_mentor_mode,
|
|
33
42
|
ecosystem_auto_share_display,
|
|
34
43
|
get_settings,
|
|
35
44
|
init_settings,
|
|
@@ -38,14 +47,33 @@ from .config import (
|
|
|
38
47
|
apprentice_agent_readiness_status,
|
|
39
48
|
mentor_model_provider_readiness,
|
|
40
49
|
mentor_mode_display,
|
|
50
|
+
mentor_mode_to_evaluation_mode,
|
|
51
|
+
normalize_apprenticeship_mode,
|
|
41
52
|
normalize_mentor_mode,
|
|
42
53
|
normalize_ecosystem_auto_share,
|
|
54
|
+
normalize_ecosystem_storage,
|
|
43
55
|
normalize_sensitive_info_masking,
|
|
56
|
+
normalize_training_contribution_mode,
|
|
57
|
+
training_contribution_mode_display,
|
|
44
58
|
debug_settings,
|
|
45
59
|
public_settings,
|
|
46
60
|
public_settings_text,
|
|
47
61
|
update_settings,
|
|
48
62
|
)
|
|
63
|
+
from .ecosystem_r2 import (
|
|
64
|
+
append_successful_share,
|
|
65
|
+
download_r2_package,
|
|
66
|
+
ecosystem_item_from_bundle,
|
|
67
|
+
fetch_r2_index,
|
|
68
|
+
fetch_r2_item,
|
|
69
|
+
get_or_create_ecosystem_identity,
|
|
70
|
+
r2_api_url,
|
|
71
|
+
r2_status,
|
|
72
|
+
shared_history,
|
|
73
|
+
storage_ref_for_item,
|
|
74
|
+
upload_contribution_to_worker,
|
|
75
|
+
write_storage_ref,
|
|
76
|
+
)
|
|
49
77
|
from .openai_structured import run_llm_smoke, format_smoke_counters
|
|
50
78
|
from .env import contains_secret, redact_secrets
|
|
51
79
|
from .public_sanitizer import sanitize_public_obj, sanitize_public_text
|
|
@@ -56,6 +84,9 @@ from .learning import (
|
|
|
56
84
|
active_packs,
|
|
57
85
|
compare_replay,
|
|
58
86
|
compile_experience_pack,
|
|
87
|
+
install_runtime_training_from_bundle,
|
|
88
|
+
installed_skill_run_refs,
|
|
89
|
+
list_installed_skills,
|
|
59
90
|
list_packs,
|
|
60
91
|
load_pack,
|
|
61
92
|
pack_run_refs,
|
|
@@ -64,7 +95,10 @@ from .learning import (
|
|
|
64
95
|
resolve_learning_source,
|
|
65
96
|
resolve_packs_for_run,
|
|
66
97
|
search_learning_sources,
|
|
98
|
+
select_runtime_training_for_run,
|
|
99
|
+
set_installed_skill_enabled,
|
|
67
100
|
slugify,
|
|
101
|
+
uninstall_installed_skill,
|
|
68
102
|
update_pack_status,
|
|
69
103
|
write_before_after_result,
|
|
70
104
|
)
|
|
@@ -73,11 +107,13 @@ from .progress import append_progress_event, format_progress_event, format_run_s
|
|
|
73
107
|
from .recipes import MODEL_PROVIDER_RECIPES, WORKER_AGENT_RECIPES
|
|
74
108
|
app=typer.Typer(add_completion=False)
|
|
75
109
|
configure_app=typer.Typer(add_completion=False, help='Configure Apprentice Agents and Mentor Model Providers.')
|
|
76
|
-
ecosystem_app=typer.Typer(add_completion=False, help='Explore and
|
|
77
|
-
|
|
110
|
+
ecosystem_app=typer.Typer(add_completion=False, help='Explore and share Agent Apprenticeship Experience Compilations.')
|
|
111
|
+
ecosystem_shared_app=typer.Typer(add_completion=False, help='Inspect local shared Experience Compilation history.')
|
|
112
|
+
bundle_app=typer.Typer(add_completion=False, help='Inspect local Experience Compilations.')
|
|
78
113
|
learn_app=typer.Typer(add_completion=False, help='Create and apply reversible Experience Packs from ecosystem experience.')
|
|
79
114
|
app.add_typer(configure_app, name='configure')
|
|
80
115
|
app.add_typer(ecosystem_app, name='ecosystem')
|
|
116
|
+
ecosystem_app.add_typer(ecosystem_shared_app, name='shared')
|
|
81
117
|
app.add_typer(bundle_app, name='bundle')
|
|
82
118
|
app.add_typer(learn_app, name='learn')
|
|
83
119
|
|
|
@@ -102,9 +138,9 @@ def _split_asset_prompt(text: str) -> list[Path]:
|
|
|
102
138
|
def _print_bundle_ready(bundle: Path | None) -> None:
|
|
103
139
|
if not bundle:
|
|
104
140
|
return
|
|
105
|
-
typer.echo(f'
|
|
141
|
+
typer.echo(f'Experience Compilation path: {bundle}')
|
|
106
142
|
typer.echo(f'Contribute to public ecosystem: apprentice ecosystem contribute {bundle}')
|
|
107
|
-
typer.echo(
|
|
143
|
+
typer.echo('Public ecosystem: Forsy ecosystem')
|
|
108
144
|
typer.echo(f'View files: open {bundle}')
|
|
109
145
|
|
|
110
146
|
|
|
@@ -113,7 +149,7 @@ def _print_public_ecosystem_contribution_help(bundle: str | Path) -> None:
|
|
|
113
149
|
typer.echo(f'apprentice ecosystem contribute {bundle}')
|
|
114
150
|
typer.echo('')
|
|
115
151
|
typer.echo('Public ecosystem:')
|
|
116
|
-
typer.echo(
|
|
152
|
+
typer.echo('Forsy ecosystem')
|
|
117
153
|
|
|
118
154
|
|
|
119
155
|
def _print_first_run_banner() -> None:
|
|
@@ -156,15 +192,14 @@ def _print_task_result(
|
|
|
156
192
|
typer.echo(f'{label}:')
|
|
157
193
|
typer.echo(str(artifacts))
|
|
158
194
|
if bundle:
|
|
159
|
-
typer.echo('
|
|
195
|
+
typer.echo('Experience Compilation:')
|
|
160
196
|
typer.echo(str(bundle))
|
|
161
197
|
if include_contribution_help:
|
|
162
198
|
typer.echo('')
|
|
163
|
-
mode = get_settings().
|
|
164
|
-
|
|
199
|
+
mode = get_settings().training_contribution_mode
|
|
200
|
+
typer.echo(f'Agent Training Contribution Mode: {training_contribution_mode_display(mode)}')
|
|
201
|
+
if mode == "private_internal_only":
|
|
165
202
|
_print_public_ecosystem_contribution_help(bundle)
|
|
166
|
-
else:
|
|
167
|
-
typer.echo(f'Ecosystem Auto-Share: {ecosystem_auto_share_display(mode)}')
|
|
168
203
|
typer.echo('')
|
|
169
204
|
typer.echo('View files:')
|
|
170
205
|
typer.echo(f'open {bundle}')
|
|
@@ -184,15 +219,14 @@ def _print_session_finished(status: dict, *, include_contribution_help: bool=Tru
|
|
|
184
219
|
typer.echo("Artifacts:")
|
|
185
220
|
typer.echo(str(status.get('artifacts_path')))
|
|
186
221
|
if status.get('contribution_bundle_path'):
|
|
187
|
-
typer.echo("
|
|
222
|
+
typer.echo("Experience Compilation:")
|
|
188
223
|
typer.echo(str(status.get('contribution_bundle_path')))
|
|
189
224
|
if include_contribution_help:
|
|
190
225
|
typer.echo('')
|
|
191
|
-
mode = get_settings().
|
|
192
|
-
|
|
226
|
+
mode = get_settings().training_contribution_mode
|
|
227
|
+
typer.echo(f'Agent Training Contribution Mode: {training_contribution_mode_display(mode)}')
|
|
228
|
+
if mode == "private_internal_only":
|
|
193
229
|
_print_public_ecosystem_contribution_help(status.get('contribution_bundle_path'))
|
|
194
|
-
else:
|
|
195
|
-
typer.echo(f'Ecosystem Auto-Share: {ecosystem_auto_share_display(mode)}')
|
|
196
230
|
typer.echo('')
|
|
197
231
|
typer.echo('View files:')
|
|
198
232
|
typer.echo(f"open {status.get('contribution_bundle_path')}")
|
|
@@ -327,6 +361,7 @@ def _configure_detected_model_provider(row: dict) -> None:
|
|
|
327
361
|
|
|
328
362
|
def _first_run_setup(*, interactive: bool, defaults: bool=False) -> None:
|
|
329
363
|
detected_agents = _detect_apprentice_agents()
|
|
364
|
+
detected_providers = _detect_model_provider_keys()
|
|
330
365
|
typer.echo("")
|
|
331
366
|
if detected_agents:
|
|
332
367
|
typer.echo("Detected Apprentice Agents:")
|
|
@@ -351,7 +386,6 @@ def _first_run_setup(*, interactive: bool, defaults: bool=False) -> None:
|
|
|
351
386
|
typer.echo("Or choose Custom to provide a command template.")
|
|
352
387
|
typer.echo("Then rerun: apprentice init --setup")
|
|
353
388
|
|
|
354
|
-
detected_providers = _detect_model_provider_keys()
|
|
355
389
|
typer.echo("")
|
|
356
390
|
if detected_providers:
|
|
357
391
|
typer.echo("Detected Mentor Model Provider keys:")
|
|
@@ -370,6 +404,37 @@ def _first_run_setup(*, interactive: bool, defaults: bool=False) -> None:
|
|
|
370
404
|
typer.echo("No Mentor Model Provider API key was detected.")
|
|
371
405
|
_print_key_storage_guidance()
|
|
372
406
|
|
|
407
|
+
typer.echo("")
|
|
408
|
+
default_mode = "autonomous" if detected_providers else "expert_led"
|
|
409
|
+
if defaults:
|
|
410
|
+
mode = default_mode
|
|
411
|
+
update_settings(apprenticeship_mode=mode, training_contribution_mode="public_ecosystem")
|
|
412
|
+
typer.echo(f"Configured Apprenticeship Mode: {apprenticeship_mode_display(mode)}")
|
|
413
|
+
typer.echo("Configured Agent Training Contribution Mode: Public Ecosystem")
|
|
414
|
+
elif interactive and sys.stdin.isatty():
|
|
415
|
+
typer.echo("Choose Apprenticeship Mode:")
|
|
416
|
+
typer.echo("1. Autonomous Apprenticeship")
|
|
417
|
+
typer.echo("2. Expert-Led Apprenticeship")
|
|
418
|
+
typer.echo("3. Organization Custom")
|
|
419
|
+
mode_choice = typer.prompt("Default", default=get_settings().apprenticeship_mode or default_mode).strip().lower()
|
|
420
|
+
mode = {"1": "autonomous", "2": "expert_led", "3": "org_custom"}.get(mode_choice, mode_choice)
|
|
421
|
+
mode = normalize_apprenticeship_mode(mode)
|
|
422
|
+
if mode == "org_custom":
|
|
423
|
+
typer.echo("Organization Custom is available through private setup. Visit https://agentapprenticeship.org/custom")
|
|
424
|
+
typer.echo("")
|
|
425
|
+
typer.echo("Choose Agent Training Contribution Mode:")
|
|
426
|
+
typer.echo("1. Public Ecosystem")
|
|
427
|
+
typer.echo("2. Private Internal Only")
|
|
428
|
+
contribution_choice = typer.prompt("Default", default=get_settings().training_contribution_mode).strip().lower()
|
|
429
|
+
contribution = {"1": "public_ecosystem", "2": "private_internal_only"}.get(contribution_choice, contribution_choice)
|
|
430
|
+
update_settings(apprenticeship_mode=mode, training_contribution_mode=contribution)
|
|
431
|
+
typer.echo(f"Configured Apprenticeship Mode: {apprenticeship_mode_display(mode)}")
|
|
432
|
+
typer.echo(f"Configured Agent Training Contribution Mode: {training_contribution_mode_display(contribution)}")
|
|
433
|
+
elif interactive:
|
|
434
|
+
update_settings(apprenticeship_mode=default_mode, training_contribution_mode="public_ecosystem")
|
|
435
|
+
typer.echo(f"Configured Apprenticeship Mode: {apprenticeship_mode_display(default_mode)}")
|
|
436
|
+
typer.echo("Configured Agent Training Contribution Mode: Public Ecosystem")
|
|
437
|
+
|
|
373
438
|
|
|
374
439
|
def _progress_callback(quiet: bool=False, verbose: bool=False, json_progress: bool=False):
|
|
375
440
|
if quiet:
|
|
@@ -384,7 +449,7 @@ def _progress_callback(quiet: bool=False, verbose: bool=False, json_progress: bo
|
|
|
384
449
|
typer.echo('')
|
|
385
450
|
typer.echo(f'Run: {status.get("run_id")}')
|
|
386
451
|
typer.echo(f'Apprentice Agent: {status.get("apprentice_agent") or status.get("worker_agent")}')
|
|
387
|
-
typer.echo(f'
|
|
452
|
+
typer.echo(f'Apprenticeship Mode: {apprenticeship_mode_display(status.get("apprenticeship_mode") or status.get("mentor_mode"))}')
|
|
388
453
|
typer.echo(f'Maximum Improvement Loops: {status.get("maximum_improvement_loops")}')
|
|
389
454
|
typer.echo(f'Task workspace: {status.get("task_workspace_path")}')
|
|
390
455
|
typer.echo(f'Artifacts: {status.get("artifacts_path")}')
|
|
@@ -399,6 +464,16 @@ def _progress_callback(quiet: bool=False, verbose: bool=False, json_progress: bo
|
|
|
399
464
|
if event_type in {'followup_completed'}:
|
|
400
465
|
_print_task_result(status, followup=True, record_only=bool((event.get('metadata_json') or {}).get('record_only')))
|
|
401
466
|
return
|
|
467
|
+
if event_type == 'task_outputs_ready':
|
|
468
|
+
typer.echo('')
|
|
469
|
+
task_status = event.get('task_status') or status.get('task_status')
|
|
470
|
+
typer.echo('Task completed.' if task_status == 'completed' else 'Task outputs are available.')
|
|
471
|
+
if event.get('artifacts_path'):
|
|
472
|
+
typer.echo(f"Artifacts are available at: {event.get('artifacts_path')}")
|
|
473
|
+
return
|
|
474
|
+
if event_type in {'tdo_started','tdo_round_generated','tdo_round_judged','tdo_round_improved','tdo_completed','tdo_failed'}:
|
|
475
|
+
typer.echo(format_progress_event(event))
|
|
476
|
+
return
|
|
402
477
|
if event_type in {'apprentice_attempt_started','apprentice_attempt_completed','worker_attempt_started','worker_attempt_completed','revision_started','revision_completed','mentor_review_started','mentor_review_completed','task_workspace_prepared','contribution_bundle_completed','operational_error'} or verbose:
|
|
403
478
|
typer.echo(format_progress_event(event))
|
|
404
479
|
return _emit
|
|
@@ -460,7 +535,7 @@ def _normalize_evaluation_mode(value: str | None) -> str | None:
|
|
|
460
535
|
return None
|
|
461
536
|
normalized=value.strip().lower().replace('_','-')
|
|
462
537
|
if normalized not in EVALUATION_MODES:
|
|
463
|
-
raise typer.BadParameter(f'
|
|
538
|
+
raise typer.BadParameter(f'apprenticeship mode must be one of: {", ".join(APPRENTICESHIP_MODES)}')
|
|
464
539
|
return normalized
|
|
465
540
|
|
|
466
541
|
def _normalize_mentor_mode(value: str | None) -> str | None:
|
|
@@ -469,7 +544,7 @@ def _normalize_mentor_mode(value: str | None) -> str | None:
|
|
|
469
544
|
try:
|
|
470
545
|
return normalize_mentor_mode(value)
|
|
471
546
|
except ValueError as exc:
|
|
472
|
-
raise typer.BadParameter(f'
|
|
547
|
+
raise typer.BadParameter(f'apprenticeship mode must be one of: {", ".join(APPRENTICESHIP_MODES)}') from exc
|
|
473
548
|
|
|
474
549
|
def _normalize_data_sharing(value: str | None) -> str | None:
|
|
475
550
|
if value is None:
|
|
@@ -501,10 +576,33 @@ def _normalize_auto_share(value: str | None) -> str | None:
|
|
|
501
576
|
return normalize_ecosystem_auto_share(value)
|
|
502
577
|
except ValueError as exc:
|
|
503
578
|
raise typer.BadParameter(
|
|
504
|
-
f'
|
|
579
|
+
f'legacy sharing alias must be one of: {", ".join(ECOSYSTEM_AUTO_SHARE_MODES)}'
|
|
580
|
+
) from exc
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _normalize_apprenticeship_mode(value: str | None) -> str | None:
|
|
584
|
+
if value is None:
|
|
585
|
+
return None
|
|
586
|
+
try:
|
|
587
|
+
return normalize_apprenticeship_mode(value)
|
|
588
|
+
except ValueError as exc:
|
|
589
|
+
raise typer.BadParameter(
|
|
590
|
+
f'apprenticeship mode must be one of: {", ".join(APPRENTICESHIP_MODES)}'
|
|
591
|
+
) from exc
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _normalize_training_contribution(value: str | None) -> str | None:
|
|
595
|
+
if value is None:
|
|
596
|
+
return None
|
|
597
|
+
try:
|
|
598
|
+
return normalize_training_contribution_mode(value)
|
|
599
|
+
except ValueError as exc:
|
|
600
|
+
raise typer.BadParameter(
|
|
601
|
+
f'agent training contribution mode must be one of: {", ".join(TRAINING_CONTRIBUTION_MODES)}'
|
|
505
602
|
) from exc
|
|
506
603
|
|
|
507
604
|
def _settings_for_run(
|
|
605
|
+
apprenticeship_mode: str | None=None,
|
|
508
606
|
mentor_mode: str | None=None,
|
|
509
607
|
sensitive_info_masking: str | None=None,
|
|
510
608
|
max_loops: int | None=None,
|
|
@@ -513,12 +611,15 @@ def _settings_for_run(
|
|
|
513
611
|
):
|
|
514
612
|
settings=get_settings()
|
|
515
613
|
updates={}
|
|
614
|
+
legacy_mentor_mode = _normalize_mentor_mode(mentor_mode) if mentor_mode is not None else None
|
|
615
|
+
if apprenticeship_mode is not None:
|
|
616
|
+
updates['apprenticeship_mode']=_normalize_apprenticeship_mode(apprenticeship_mode)
|
|
516
617
|
if mentor_mode is not None:
|
|
517
|
-
updates['
|
|
618
|
+
updates['apprenticeship_mode']=_normalize_apprenticeship_mode(mentor_mode)
|
|
518
619
|
if evaluation_mode is not None:
|
|
519
620
|
updates['evaluation_mode']=_normalize_evaluation_mode(evaluation_mode)
|
|
520
|
-
if mentor_mode is None:
|
|
521
|
-
updates['
|
|
621
|
+
if mentor_mode is None and apprenticeship_mode is None:
|
|
622
|
+
updates['apprenticeship_mode']=_normalize_apprenticeship_mode(evaluation_mode)
|
|
522
623
|
if sensitive_info_masking is not None:
|
|
523
624
|
updates['sensitive_info_masking']=_normalize_masking(sensitive_info_masking)
|
|
524
625
|
if data_sharing_level is not None:
|
|
@@ -530,6 +631,12 @@ def _settings_for_run(
|
|
|
530
631
|
raise typer.BadParameter('maximum improvement loops must be at least 1')
|
|
531
632
|
updates['max_improvement_loops']=max_loops
|
|
532
633
|
updates['max_iterations']=max_loops
|
|
634
|
+
if "apprenticeship_mode" in updates:
|
|
635
|
+
updates["mentor_mode"] = apprenticeship_mode_to_mentor_mode(updates["apprenticeship_mode"])
|
|
636
|
+
updates["evaluation_mode"] = mentor_mode_to_evaluation_mode(updates["mentor_mode"])
|
|
637
|
+
if legacy_mentor_mode == "hybrid" and apprenticeship_mode is None:
|
|
638
|
+
updates["mentor_mode"] = "hybrid"
|
|
639
|
+
updates["evaluation_mode"] = "hybrid"
|
|
533
640
|
return settings.model_copy(update=updates)
|
|
534
641
|
|
|
535
642
|
def _configure_model_impl(provider: str | None, model: str | None, api_key_env_var: str | None, test_connection: bool):
|
|
@@ -609,15 +716,15 @@ def _ensure_evaluation_ready(settings, interactive: bool=True):
|
|
|
609
716
|
if settings.mentor_mode == 'expert_led' or model_provider_ready(settings):
|
|
610
717
|
return settings
|
|
611
718
|
if not interactive:
|
|
612
|
-
raise typer.BadParameter('
|
|
719
|
+
raise typer.BadParameter('Autonomous Apprenticeship requires a ready Mentor Model Provider; use configure model or --apprenticeship-mode expert-led')
|
|
613
720
|
if not _stdin_allows_prompt():
|
|
614
721
|
readiness = mentor_model_provider_readiness(settings)
|
|
615
722
|
reason = readiness.get("reason") or "Mentor Model Provider is not ready."
|
|
616
723
|
raise typer.BadParameter(
|
|
617
|
-
f"
|
|
618
|
-
"Run `apprentice doctor --live`, `apprentice configure model`, or use `--
|
|
724
|
+
f"Autonomous Apprenticeship requires a ready Mentor Model Provider. {reason} "
|
|
725
|
+
"Run `apprentice doctor --live`, `apprentice configure model`, or use `--apprenticeship-mode expert-led`."
|
|
619
726
|
)
|
|
620
|
-
typer.echo('
|
|
727
|
+
typer.echo('Autonomous Apprenticeship needs a ready Mentor Model Provider.')
|
|
621
728
|
readiness=mentor_model_provider_readiness(settings)
|
|
622
729
|
if readiness.get("provider"):
|
|
623
730
|
typer.echo(_status_line('Mentor Model Provider Status', readiness.get("status"), readiness.get("reason")))
|
|
@@ -632,14 +739,14 @@ def _ensure_evaluation_ready(settings, interactive: bool=True):
|
|
|
632
739
|
except typer.Abort:
|
|
633
740
|
reason = readiness.get("reason") or "Mentor Model Provider is not ready."
|
|
634
741
|
raise typer.BadParameter(
|
|
635
|
-
f"
|
|
636
|
-
"Run `apprentice doctor --live`, `apprentice configure model`, or use `--
|
|
742
|
+
f"Autonomous Apprenticeship requires a ready Mentor Model Provider. {reason} "
|
|
743
|
+
"Run `apprentice doctor --live`, `apprentice configure model`, or use `--apprenticeship-mode expert-led`."
|
|
637
744
|
) from None
|
|
638
745
|
if choice in {'1','setup','set up','configure','configure mentor model provider','set up model provider now'}:
|
|
639
746
|
configured=_configure_model_impl(None, None, None, test_connection=False)
|
|
640
747
|
return configured
|
|
641
748
|
if choice in {'2','expert-led','expert','manual','run this session in expert-led mode'}:
|
|
642
|
-
return settings.model_copy(update={'mentor_mode':'expert_led', 'evaluation_mode':'expert-led'})
|
|
749
|
+
return settings.model_copy(update={'apprenticeship_mode':'expert_led', 'mentor_mode':'expert_led', 'evaluation_mode':'expert-led'})
|
|
643
750
|
raise typer.Exit(1)
|
|
644
751
|
|
|
645
752
|
|
|
@@ -727,6 +834,7 @@ def init(
|
|
|
727
834
|
):
|
|
728
835
|
_print_first_run_banner()
|
|
729
836
|
path=init_settings(overwrite=overwrite)
|
|
837
|
+
get_or_create_ecosystem_identity(get_settings())
|
|
730
838
|
typer.echo(f'initialized Agent Apprenticeship settings at {path}')
|
|
731
839
|
if defaults:
|
|
732
840
|
_first_run_setup(interactive=False, defaults=True)
|
|
@@ -774,7 +882,7 @@ def doctor(
|
|
|
774
882
|
typer.echo(f'Apprentice Agent command: {agent.get("command") or "Not configured"}')
|
|
775
883
|
typer.echo(f'Apprentice Agent command found: {_yes_no(bool(agent.get("command_found")))}')
|
|
776
884
|
typer.echo(_status_line('Apprentice Agent readiness', agent.get('status'), agent.get('reason')))
|
|
777
|
-
typer.echo(f'
|
|
885
|
+
typer.echo(f'Apprenticeship Mode: {apprenticeship_mode_display(settings.apprenticeship_mode)}')
|
|
778
886
|
typer.echo(f'Mentor Model Provider: {provider.get("provider_display")}')
|
|
779
887
|
typer.echo(f'Mentor model: {provider.get("model") or "Not configured"}')
|
|
780
888
|
typer.echo(f'API key env var: {provider.get("api_key_env_var") or "Not configured"}')
|
|
@@ -1209,13 +1317,16 @@ def configure_agent(
|
|
|
1209
1317
|
|
|
1210
1318
|
@configure_app.command('settings')
|
|
1211
1319
|
def configure_settings(
|
|
1212
|
-
|
|
1320
|
+
apprenticeship_mode: str | None=typer.Option(None, '--apprenticeship-mode', help='autonomous, expert-led, or org-custom.'),
|
|
1321
|
+
mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
|
|
1213
1322
|
sensitive_info_masking: str | None=typer.Option(None, '--sensitive-info-masking', help='standard or no-masking.'),
|
|
1214
1323
|
max_loops: int | None=typer.Option(None, '--max-loops', help='Maximum improvement loops.'),
|
|
1215
1324
|
):
|
|
1216
1325
|
updates={}
|
|
1326
|
+
if apprenticeship_mode is not None:
|
|
1327
|
+
updates['apprenticeship_mode']=_normalize_apprenticeship_mode(apprenticeship_mode)
|
|
1217
1328
|
if mentor_mode is not None:
|
|
1218
|
-
updates['
|
|
1329
|
+
updates['apprenticeship_mode']=_normalize_apprenticeship_mode(mentor_mode)
|
|
1219
1330
|
if sensitive_info_masking is not None:
|
|
1220
1331
|
updates['sensitive_info_masking']=_normalize_masking(sensitive_info_masking)
|
|
1221
1332
|
if max_loops is not None:
|
|
@@ -1228,6 +1339,46 @@ def configure_settings(
|
|
|
1228
1339
|
update_settings(**updates)
|
|
1229
1340
|
typer.echo(public_settings_text())
|
|
1230
1341
|
|
|
1342
|
+
|
|
1343
|
+
@configure_app.command('apprenticeship')
|
|
1344
|
+
def configure_apprenticeship(
|
|
1345
|
+
mode: str | None=typer.Argument(None, help='autonomous, expert-led, or org-custom.'),
|
|
1346
|
+
):
|
|
1347
|
+
if mode is None:
|
|
1348
|
+
typer.echo("Apprenticeship Mode options:")
|
|
1349
|
+
typer.echo("- autonomous: Autonomous Apprenticeship")
|
|
1350
|
+
typer.echo("- expert-led: Expert-Led Apprenticeship")
|
|
1351
|
+
typer.echo("- org-custom: Organization Custom")
|
|
1352
|
+
mode = typer.prompt("Apprenticeship Mode")
|
|
1353
|
+
normalized = _normalize_apprenticeship_mode(mode)
|
|
1354
|
+
updated = update_settings(apprenticeship_mode=normalized)
|
|
1355
|
+
typer.echo(f"Apprenticeship Mode: {apprenticeship_mode_display(updated.apprenticeship_mode)}")
|
|
1356
|
+
if updated.apprenticeship_mode == "org_custom":
|
|
1357
|
+
typer.echo("Organization Custom is available through private setup. Visit https://agentapprenticeship.org/custom")
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
@configure_app.command('contribution')
|
|
1361
|
+
def configure_contribution(
|
|
1362
|
+
mode: str | None=typer.Argument(None, help='public-ecosystem or private-internal-only.'),
|
|
1363
|
+
sharing_behavior: str | None=typer.Option(None, "--sharing-behavior", help="Public Ecosystem behavior: manual or automatic."),
|
|
1364
|
+
):
|
|
1365
|
+
if mode is None:
|
|
1366
|
+
typer.echo("Agent Training Contribution Mode options:")
|
|
1367
|
+
typer.echo("- public-ecosystem: share completed Experience Compilations to the Forsy ecosystem")
|
|
1368
|
+
typer.echo("- private-internal-only: keep completed packages local")
|
|
1369
|
+
mode = typer.prompt("Agent Training Contribution Mode")
|
|
1370
|
+
normalized = _normalize_training_contribution(mode)
|
|
1371
|
+
updates = {"training_contribution_mode": normalized}
|
|
1372
|
+
if sharing_behavior is not None:
|
|
1373
|
+
behavior = _normalize_auto_share(sharing_behavior)
|
|
1374
|
+
updates["ecosystem_auto_share"] = behavior
|
|
1375
|
+
if behavior != "disabled":
|
|
1376
|
+
updates["training_contribution_mode"] = "public_ecosystem"
|
|
1377
|
+
updated = update_settings(**updates)
|
|
1378
|
+
typer.echo(f"Agent Training Contribution Mode: {training_contribution_mode_display(updated.training_contribution_mode)}")
|
|
1379
|
+
if updated.training_contribution_mode == "public_ecosystem":
|
|
1380
|
+
typer.echo(f"Public Ecosystem sharing behavior: {ecosystem_auto_share_display(updated.ecosystem_auto_share)}")
|
|
1381
|
+
|
|
1231
1382
|
@configure_app.command('model')
|
|
1232
1383
|
def configure_model(
|
|
1233
1384
|
provider: str | None=typer.Argument(None, help='Known Mentor Model Provider id.'),
|
|
@@ -1241,18 +1392,19 @@ def configure_model(
|
|
|
1241
1392
|
def start(
|
|
1242
1393
|
asset: list[Path]=typer.Option([], '--asset', '-a', help='Optional task asset file or directory.'),
|
|
1243
1394
|
runner: str | None=typer.Option(None, '--runner', help='Advanced compatibility runner override.'),
|
|
1244
|
-
|
|
1395
|
+
apprenticeship_mode: str | None=typer.Option(None, '--apprenticeship-mode', help='autonomous, expert-led, or org-custom.'),
|
|
1396
|
+
mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
|
|
1245
1397
|
sensitive_info_masking: str | None=typer.Option(None, '--sensitive-info-masking', help='standard or no-masking.'),
|
|
1246
|
-
evaluation_mode: str | None=typer.Option(None, '--evaluation-mode', help='Backward-compatible alias for --
|
|
1247
|
-
data_sharing_level: str | None=typer.Option(None, '--data-sharing-level', help='Backward-compatible alias for --sensitive-info-masking.'),
|
|
1398
|
+
evaluation_mode: str | None=typer.Option(None, '--evaluation-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
|
|
1399
|
+
data_sharing_level: str | None=typer.Option(None, '--data-sharing-level', help='Backward-compatible alias for --sensitive-info-masking.', hidden=True),
|
|
1248
1400
|
max_loops: int | None=typer.Option(None, '--max-loops', help='Maximum improvement loops for this run.'),
|
|
1249
1401
|
quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
|
|
1250
1402
|
verbose: bool=typer.Option(False, '--verbose', help='Print additional progress details when available.'),
|
|
1251
1403
|
json_progress: bool=typer.Option(False, '--json-progress', help='Emit progress events as JSONL.'),
|
|
1252
1404
|
expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
|
|
1253
|
-
hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='
|
|
1405
|
+
hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Compatibility test/dev helper.', hidden=True),
|
|
1254
1406
|
expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led checkpoint inputs even in non-TTY test harnesses.'),
|
|
1255
|
-
hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='
|
|
1407
|
+
hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Compatibility test/dev helper.', hidden=True),
|
|
1256
1408
|
):
|
|
1257
1409
|
current_settings = get_settings()
|
|
1258
1410
|
current_agent = apprentice_agent_readiness_status(current_settings)
|
|
@@ -1261,7 +1413,7 @@ def start(
|
|
|
1261
1413
|
instruction=typer.prompt('What should your agent work on?')
|
|
1262
1414
|
extra=typer.prompt('Add assets? optional', default='', show_default=False)
|
|
1263
1415
|
assets=list(asset)+_split_asset_prompt(extra)
|
|
1264
|
-
settings=_ensure_evaluation_ready(_settings_for_run(mentor_mode, sensitive_info_masking, max_loops, evaluation_mode, data_sharing_level), interactive=True)
|
|
1416
|
+
settings=_ensure_evaluation_ready(_settings_for_run(apprenticeship_mode, mentor_mode, sensitive_info_masking, max_loops, evaluation_mode, data_sharing_level), interactive=True)
|
|
1265
1417
|
_ensure_apprentice_agent_ready(settings, runner, interactive=True)
|
|
1266
1418
|
interactive_checkpoints = _interactive_checkpoint_requested(
|
|
1267
1419
|
settings,
|
|
@@ -1295,10 +1447,11 @@ def run_prompt(
|
|
|
1295
1447
|
asset: list[Path]=typer.Option([], '--asset', '-a', help='Optional task asset file or directory.'),
|
|
1296
1448
|
run_id: str | None=typer.Option(None, '--run-id', help='Optional run id/slug.'),
|
|
1297
1449
|
runner: str | None=typer.Option(None, '--runner', help='Advanced compatibility runner override.'),
|
|
1298
|
-
|
|
1450
|
+
apprenticeship_mode: str | None=typer.Option(None, '--apprenticeship-mode', help='autonomous, expert-led, or org-custom.'),
|
|
1451
|
+
mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
|
|
1299
1452
|
sensitive_info_masking: str | None=typer.Option(None, '--sensitive-info-masking', help='standard or no-masking.'),
|
|
1300
|
-
evaluation_mode: str | None=typer.Option(None, '--evaluation-mode', help='Backward-compatible alias for --
|
|
1301
|
-
data_sharing_level: str | None=typer.Option(None, '--data-sharing-level', help='Backward-compatible alias for --sensitive-info-masking.'),
|
|
1453
|
+
evaluation_mode: str | None=typer.Option(None, '--evaluation-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
|
|
1454
|
+
data_sharing_level: str | None=typer.Option(None, '--data-sharing-level', help='Backward-compatible alias for --sensitive-info-masking.', hidden=True),
|
|
1302
1455
|
max_loops: int | None=typer.Option(None, '--max-loops', help='Maximum improvement loops for this run.'),
|
|
1303
1456
|
quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
|
|
1304
1457
|
verbose: bool=typer.Option(False, '--verbose', help='Print additional progress details when available.'),
|
|
@@ -1306,12 +1459,14 @@ def run_prompt(
|
|
|
1306
1459
|
experience_pack: list[str]=typer.Option([], '--experience-pack', help='Experience Pack id/path to apply to this run.'),
|
|
1307
1460
|
no_experience_packs: bool=typer.Option(False, '--no-experience-packs', help='Do not apply Experience Packs to this run.'),
|
|
1308
1461
|
use_active_experience_packs: bool=typer.Option(False, '--use-active-experience-packs', help='Apply active Experience Packs to this run.'),
|
|
1462
|
+
use_installed_skills: bool=typer.Option(False, '--use-installed-skills', help='Force relevant installed runtime training into this run.'),
|
|
1463
|
+
no_installed_skills: bool=typer.Option(False, '--no-installed-skills', help='Do not apply installed runtime training to this run.'),
|
|
1309
1464
|
expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
|
|
1310
|
-
hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='
|
|
1465
|
+
hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Compatibility test/dev helper.', hidden=True),
|
|
1311
1466
|
expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led checkpoint inputs even in non-TTY test harnesses.'),
|
|
1312
|
-
hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='
|
|
1467
|
+
hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Compatibility test/dev helper.', hidden=True),
|
|
1313
1468
|
):
|
|
1314
|
-
settings=_ensure_evaluation_ready(_settings_for_run(mentor_mode, sensitive_info_masking, max_loops, evaluation_mode, data_sharing_level), interactive=True)
|
|
1469
|
+
settings=_ensure_evaluation_ready(_settings_for_run(apprenticeship_mode, mentor_mode, sensitive_info_masking, max_loops, evaluation_mode, data_sharing_level), interactive=True)
|
|
1315
1470
|
_ensure_apprentice_agent_ready(settings, runner, interactive=True)
|
|
1316
1471
|
for pack_id in experience_pack:
|
|
1317
1472
|
try:
|
|
@@ -1327,8 +1482,26 @@ def run_prompt(
|
|
|
1327
1482
|
no_packs=no_experience_packs,
|
|
1328
1483
|
settings=settings,
|
|
1329
1484
|
)
|
|
1330
|
-
|
|
1485
|
+
installed_skills, runtime_guidance = ([], "")
|
|
1486
|
+
if not no_installed_skills:
|
|
1487
|
+
installed_skills, runtime_guidance = select_runtime_training_for_run(
|
|
1488
|
+
instruction,
|
|
1489
|
+
settings=settings,
|
|
1490
|
+
force=use_installed_skills,
|
|
1491
|
+
)
|
|
1492
|
+
if installed_skills and not quiet and not json_progress:
|
|
1493
|
+
typer.echo("Installed Runtime Training selected:")
|
|
1494
|
+
for skill in installed_skills:
|
|
1495
|
+
typer.echo(f"- {skill.get('installed_skill_id')}: {skill.get('title')}")
|
|
1496
|
+
effective_instruction = instruction.rstrip()
|
|
1497
|
+
if pack_guidance:
|
|
1498
|
+
effective_instruction += pack_guidance
|
|
1499
|
+
if runtime_guidance:
|
|
1500
|
+
effective_instruction += runtime_guidance
|
|
1501
|
+
if effective_instruction == instruction.rstrip():
|
|
1502
|
+
effective_instruction = instruction
|
|
1331
1503
|
experience_refs = pack_run_refs(selected_packs)
|
|
1504
|
+
runtime_refs = installed_skill_run_refs(installed_skills)
|
|
1332
1505
|
interactive_checkpoints = _interactive_checkpoint_requested(
|
|
1333
1506
|
settings,
|
|
1334
1507
|
expert_auto_approve=expert_auto_approve,
|
|
@@ -1350,6 +1523,7 @@ def run_prompt(
|
|
|
1350
1523
|
runner=runner,
|
|
1351
1524
|
progress_callback=_progress_callback(quiet, verbose, json_progress),
|
|
1352
1525
|
experience_pack_refs=experience_refs,
|
|
1526
|
+
runtime_training_refs=runtime_refs,
|
|
1353
1527
|
)
|
|
1354
1528
|
except RunInterrupted as exc:
|
|
1355
1529
|
if not quiet and not json_progress:
|
|
@@ -1369,15 +1543,15 @@ def continue_run(
|
|
|
1369
1543
|
followup_instruction: str | None=typer.Argument(None, help='Optional direct follow-up instruction.'),
|
|
1370
1544
|
asset: list[Path]=typer.Option([], '--asset', '-a', help='Optional follow-up asset file or directory.'),
|
|
1371
1545
|
run_loop: bool=typer.Option(False, '--run-loop', help='Run another improvement loop after recording the follow-up.'),
|
|
1372
|
-
record_only: bool=typer.Option(False, '--record-only', '--no-run-loop', help='Only record the follow-up and update the
|
|
1546
|
+
record_only: bool=typer.Option(False, '--record-only', '--no-run-loop', help='Only record the follow-up and update the Experience Compilation.'),
|
|
1373
1547
|
runner: str | None=typer.Option(None, '--runner', help='Advanced compatibility runner override.'),
|
|
1374
1548
|
quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
|
|
1375
1549
|
verbose: bool=typer.Option(False, '--verbose', help='Print additional progress details when available.'),
|
|
1376
1550
|
json_progress: bool=typer.Option(False, '--json-progress', help='Emit progress events as JSONL.'),
|
|
1377
1551
|
expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led checkpoint inputs even in non-TTY test harnesses.'),
|
|
1378
|
-
hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='
|
|
1552
|
+
hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Compatibility test/dev helper.', hidden=True),
|
|
1379
1553
|
expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
|
|
1380
|
-
hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='
|
|
1554
|
+
hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Compatibility test/dev helper.', hidden=True),
|
|
1381
1555
|
):
|
|
1382
1556
|
settings=get_settings()
|
|
1383
1557
|
run_root=run_root_for(run_id, settings)
|
|
@@ -1429,7 +1603,7 @@ def continue_run(
|
|
|
1429
1603
|
typer.echo("")
|
|
1430
1604
|
typer.echo("Follow-up interrupted.")
|
|
1431
1605
|
if previous_status.get("contribution_bundle_path"):
|
|
1432
|
-
typer.echo("Previous
|
|
1606
|
+
typer.echo("Previous Experience Compilation:")
|
|
1433
1607
|
typer.echo(str(previous_status.get("contribution_bundle_path")))
|
|
1434
1608
|
raise typer.Exit(130)
|
|
1435
1609
|
status = read_run_status(run_root)
|
|
@@ -1443,9 +1617,9 @@ def finish(
|
|
|
1443
1617
|
quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
|
|
1444
1618
|
json_progress: bool=typer.Option(False, '--json-progress', help='Emit progress events as JSONL.'),
|
|
1445
1619
|
expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led final checkpoint inputs even in non-TTY test harnesses.'),
|
|
1446
|
-
hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='
|
|
1620
|
+
hybrid_interactive: bool=typer.Option(False, '--hybrid-interactive', help='Compatibility test/dev helper.', hidden=True),
|
|
1447
1621
|
expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
|
|
1448
|
-
hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='
|
|
1622
|
+
hybrid_auto_approve: bool=typer.Option(False, '--hybrid-auto-approve', help='Compatibility test/dev helper.', hidden=True),
|
|
1449
1623
|
):
|
|
1450
1624
|
settings = get_settings()
|
|
1451
1625
|
interactive_checkpoints = _interactive_checkpoint_requested(
|
|
@@ -1460,7 +1634,7 @@ def finish(
|
|
|
1460
1634
|
hybrid_auto_approve=hybrid_auto_approve,
|
|
1461
1635
|
mentor_interactive_checkpoints=interactive_checkpoints,
|
|
1462
1636
|
):
|
|
1463
|
-
run_root,bundle=finish_session(run_id, settings=settings)
|
|
1637
|
+
run_root,bundle=finish_session(run_id, settings=settings, progress_callback=None if json_progress else _progress_callback(quiet, False, False))
|
|
1464
1638
|
status=read_run_status(run_root)
|
|
1465
1639
|
if json_progress:
|
|
1466
1640
|
for event in read_jsonl(run_root/'progress_events.jsonl'):
|
|
@@ -1533,6 +1707,8 @@ def _ecosystem_status() -> dict:
|
|
|
1533
1707
|
repo_path = _configured_ecosystem_repo_path()
|
|
1534
1708
|
gh_installed = bool(_gh_path())
|
|
1535
1709
|
gh_authed = _gh_authenticated() if gh_installed else False
|
|
1710
|
+
settings = get_settings()
|
|
1711
|
+
r2 = r2_status(settings)
|
|
1536
1712
|
return {
|
|
1537
1713
|
"repo": repo,
|
|
1538
1714
|
"repo_url": _ecosystem_repo_url(repo),
|
|
@@ -1541,10 +1717,22 @@ def _ecosystem_status() -> dict:
|
|
|
1541
1717
|
"gh_installed": gh_installed,
|
|
1542
1718
|
"gh_authenticated": gh_authed,
|
|
1543
1719
|
"automatic_contribution_ready": bool(repo and gh_installed and gh_authed),
|
|
1720
|
+
**r2,
|
|
1544
1721
|
}
|
|
1545
1722
|
|
|
1546
1723
|
|
|
1547
1724
|
def _ecosystem_auto_share_readiness(repo: str | None = None) -> dict:
|
|
1725
|
+
settings = get_settings()
|
|
1726
|
+
if settings.ecosystem_storage_backend == "forsy-r2":
|
|
1727
|
+
api_url = r2_api_url(settings)
|
|
1728
|
+
return {
|
|
1729
|
+
"ready": bool(api_url),
|
|
1730
|
+
"reason": None if api_url else "R2 ingestion API is not configured",
|
|
1731
|
+
"next_action": "apprentice ecosystem configure --r2-api-url <url>" if not api_url else None,
|
|
1732
|
+
"repo": _configured_ecosystem_repo(),
|
|
1733
|
+
"repo_url": _ecosystem_repo_url(),
|
|
1734
|
+
"storage_backend": "forsy-r2",
|
|
1735
|
+
}
|
|
1548
1736
|
configured_repo = repo or _configured_ecosystem_repo() or DEFAULT_PUBLIC_ECOSYSTEM_REPO
|
|
1549
1737
|
gh_installed = bool(_gh_path())
|
|
1550
1738
|
gh_authed = _gh_authenticated() if gh_installed else False
|
|
@@ -1592,13 +1780,15 @@ def _ecosystem_auto_share_readiness(repo: str | None = None) -> dict:
|
|
|
1592
1780
|
def _ecosystem_auto_share_status_line(settings=None) -> str:
|
|
1593
1781
|
s = settings or get_settings()
|
|
1594
1782
|
readiness = _ecosystem_auto_share_readiness()
|
|
1595
|
-
label =
|
|
1596
|
-
if s.
|
|
1597
|
-
return f"
|
|
1783
|
+
label = training_contribution_mode_display(s.training_contribution_mode)
|
|
1784
|
+
if s.training_contribution_mode == "private_internal_only":
|
|
1785
|
+
return f"Agent Training Contribution Mode: {label}"
|
|
1786
|
+
behavior = ecosystem_auto_share_display(s.ecosystem_auto_share)
|
|
1787
|
+
behavior_label = "automatic" if behavior == "automatic" else "manual"
|
|
1598
1788
|
if readiness["ready"]:
|
|
1599
|
-
return f"
|
|
1789
|
+
return f"Agent Training Contribution Mode: {label}\nPublic Ecosystem sharing behavior: {behavior_label}\nPublic Ecosystem upload status: Ready"
|
|
1600
1790
|
reason = readiness.get("reason") or "not ready"
|
|
1601
|
-
return f"
|
|
1791
|
+
return f"Agent Training Contribution Mode: {label}\nPublic Ecosystem sharing behavior: {behavior_label}\nPublic Ecosystem upload status: Not ready - {reason}"
|
|
1602
1792
|
|
|
1603
1793
|
|
|
1604
1794
|
def _public_settings_text_with_ecosystem() -> str:
|
|
@@ -1618,7 +1808,7 @@ def _public_settings_text_with_ecosystem() -> str:
|
|
|
1618
1808
|
out.append(f"GitHub URL: {_ecosystem_repo_url(settings.ecosystem_repo)}")
|
|
1619
1809
|
out.append(git_line)
|
|
1620
1810
|
inserted = True
|
|
1621
|
-
if line.startswith("
|
|
1811
|
+
if line.startswith("Agent Training Contribution Mode:"):
|
|
1622
1812
|
out.pop()
|
|
1623
1813
|
out.extend(auto_status)
|
|
1624
1814
|
if not inserted:
|
|
@@ -1901,8 +2091,16 @@ def _load_registry_file(path: Path) -> list[dict]:
|
|
|
1901
2091
|
return [row for row in rows if isinstance(row, dict)]
|
|
1902
2092
|
|
|
1903
2093
|
|
|
1904
|
-
def _load_registry(registry: Path | None=None) -> list[dict]:
|
|
2094
|
+
def _load_registry(registry: Path | None=None, storage: str | None = None) -> list[dict]:
|
|
1905
2095
|
if registry is None:
|
|
2096
|
+
backend = normalize_ecosystem_storage(storage or get_settings().ecosystem_storage_backend)
|
|
2097
|
+
if backend == "forsy-r2":
|
|
2098
|
+
try:
|
|
2099
|
+
return fetch_r2_index(get_settings())
|
|
2100
|
+
except FileNotFoundError as exc:
|
|
2101
|
+
raise FileNotFoundError(f"{exc} Fallback: apprentice ecosystem list --storage github") from exc
|
|
2102
|
+
except Exception as exc:
|
|
2103
|
+
raise FileNotFoundError(f"R2 ecosystem index could not be read. {redact_secrets(str(exc))} Fallback: apprentice ecosystem list --storage github") from exc
|
|
1906
2104
|
repo_path = _configured_ecosystem_repo_path()
|
|
1907
2105
|
if repo_path:
|
|
1908
2106
|
return _index_from_public_repo_path(repo_path)
|
|
@@ -1943,20 +2141,67 @@ def _load_registry(registry: Path | None=None) -> list[dict]:
|
|
|
1943
2141
|
return _load_registry_file(path)
|
|
1944
2142
|
|
|
1945
2143
|
|
|
2144
|
+
def _row_id(row: dict) -> str:
|
|
2145
|
+
return str(row.get("id") or row.get("bundle_id") or row.get("package_id") or row.get("seed_task_id") or row.get("task_id") or "")
|
|
2146
|
+
|
|
2147
|
+
|
|
2148
|
+
def _row_values(row: dict, *keys: str) -> list[str]:
|
|
2149
|
+
values: list[str] = []
|
|
2150
|
+
for key in keys:
|
|
2151
|
+
value = row.get(key)
|
|
2152
|
+
if isinstance(value, (list, tuple, set)):
|
|
2153
|
+
values.extend(str(item) for item in value if item not in (None, ""))
|
|
2154
|
+
elif isinstance(value, dict):
|
|
2155
|
+
values.extend(str(item) for item in value.values() if item not in (None, ""))
|
|
2156
|
+
elif value not in (None, ""):
|
|
2157
|
+
values.append(str(value))
|
|
2158
|
+
return values
|
|
2159
|
+
|
|
2160
|
+
|
|
2161
|
+
def _row_contains(row: dict, expected: str, *keys: str) -> bool:
|
|
2162
|
+
needle = str(expected or "").strip().lower()
|
|
2163
|
+
return bool(needle) and any(needle == value.lower() or needle in value.lower() for value in _row_values(row, *keys))
|
|
2164
|
+
|
|
2165
|
+
|
|
1946
2166
|
def _matches_registry_row(row: dict, query: str | None, filters: dict) -> bool:
|
|
2167
|
+
if filters.get("mine"):
|
|
2168
|
+
identity = get_or_create_ecosystem_identity(get_settings())
|
|
2169
|
+
contributor = row.get("contributor") or {}
|
|
2170
|
+
if str(contributor.get("ecosystem_user_id") or "") != str(identity.get("ecosystem_user_id")):
|
|
2171
|
+
return False
|
|
1947
2172
|
if query:
|
|
1948
|
-
haystack=' '.join(
|
|
1949
|
-
|
|
1950
|
-
|
|
2173
|
+
haystack=' '.join(_row_values(
|
|
2174
|
+
row,
|
|
2175
|
+
'id', 'item_id', 'bundle_id', 'package_id', 'experience_compilation_id',
|
|
2176
|
+
'title', 'task_title', 'summary', 'agent_apprentice_role',
|
|
2177
|
+
'expected_economic_value', 'economic_value_summary',
|
|
2178
|
+
'domain', 'domains', 'subdomain', 'subdomains', 'tags', 'surfaces', 'tools',
|
|
2179
|
+
))
|
|
1951
2180
|
if query.lower() not in haystack.lower():
|
|
1952
2181
|
return False
|
|
1953
|
-
|
|
2182
|
+
if filters.get("apprenticeship_mode"):
|
|
2183
|
+
expected = _normalize_apprenticeship_mode(str(filters["apprenticeship_mode"]))
|
|
2184
|
+
actual = row.get("apprenticeship_mode")
|
|
2185
|
+
if not actual and row.get("mentor_mode"):
|
|
2186
|
+
actual = normalize_apprenticeship_mode(str(row.get("mentor_mode")))
|
|
2187
|
+
if not actual:
|
|
2188
|
+
return False
|
|
2189
|
+
if normalize_apprenticeship_mode(str(actual or "")) != expected:
|
|
2190
|
+
return False
|
|
2191
|
+
for key in ['title','task_status','run_status']:
|
|
1954
2192
|
value=filters.get(key)
|
|
1955
2193
|
if value and str(row.get(key) or '').lower() != str(value).lower():
|
|
1956
2194
|
return False
|
|
1957
|
-
|
|
2195
|
+
if filters.get("mentor_mode"):
|
|
2196
|
+
expected = _normalize_apprenticeship_mode(str(filters["mentor_mode"]))
|
|
2197
|
+
actual = row.get("apprenticeship_mode") or row.get("mentor_mode")
|
|
2198
|
+
if not actual:
|
|
2199
|
+
return False
|
|
2200
|
+
if normalize_apprenticeship_mode(str(actual or "")) != expected:
|
|
2201
|
+
return False
|
|
2202
|
+
for key, fields in [('domain', ('domain', 'domains')), ('subdomain', ('subdomain', 'subdomains')), ('tag', ('tags',))]:
|
|
1958
2203
|
value=filters.get(key)
|
|
1959
|
-
if value and
|
|
2204
|
+
if value and not _row_contains(row, str(value), *fields):
|
|
1960
2205
|
return False
|
|
1961
2206
|
if filters.get('min_traced_steps') is not None and int(row.get('traced_steps') or 0) < filters['min_traced_steps']:
|
|
1962
2207
|
return False
|
|
@@ -1987,12 +2232,51 @@ def _matches_registry_row(row: dict, query: str | None, filters: dict) -> bool:
|
|
|
1987
2232
|
return True
|
|
1988
2233
|
|
|
1989
2234
|
|
|
1990
|
-
def
|
|
2235
|
+
def _select_registry_row(rows: list[dict]) -> None:
|
|
2236
|
+
if not sys.stdin.isatty():
|
|
2237
|
+
typer.echo("Selection requires an interactive terminal. Use `--json` to select an item non-interactively.")
|
|
2238
|
+
return
|
|
2239
|
+
choice = typer.prompt("Choose Experience Compilation", default="1").strip()
|
|
2240
|
+
try:
|
|
2241
|
+
index = int(choice)
|
|
2242
|
+
except ValueError:
|
|
2243
|
+
typer.echo("Selection cancelled.")
|
|
2244
|
+
return
|
|
2245
|
+
if index < 1 or index > len(rows):
|
|
2246
|
+
typer.echo("Selection cancelled.")
|
|
2247
|
+
return
|
|
2248
|
+
row = rows[index - 1]
|
|
2249
|
+
item_id = _row_id(row)
|
|
2250
|
+
typer.echo("")
|
|
2251
|
+
typer.echo("Choose action:")
|
|
2252
|
+
typer.echo("1. Export Full Experience Compilation")
|
|
2253
|
+
typer.echo("2. Install Runtime Training")
|
|
2254
|
+
action = typer.prompt("Action", default="1").strip()
|
|
2255
|
+
if action == "1":
|
|
2256
|
+
output = typer.prompt("Output path", default=str(get_settings().app_home / "ecosystem" / "exports" / item_id)).strip()
|
|
2257
|
+
package_root = _package_for_ecosystem_item(item_id)
|
|
2258
|
+
exported = _copy_or_zip_package(package_root, Path(output), item_id=item_id, source="ecosystem_select")
|
|
2259
|
+
typer.echo(f"Export Full Experience Compilation complete: {exported}")
|
|
2260
|
+
elif action == "2":
|
|
2261
|
+
package_root = _package_for_ecosystem_item(item_id)
|
|
2262
|
+
manifest = install_runtime_training_from_bundle(item_id, package_root, settings=get_settings())
|
|
2263
|
+
typer.echo("Install Runtime Training complete.")
|
|
2264
|
+
typer.echo(f"installed_skill_id: {manifest.get('installed_skill_id')}")
|
|
2265
|
+
else:
|
|
2266
|
+
typer.echo("Selection cancelled.")
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
def _print_registry_rows(rows: list[dict], *, json_output: bool = False, select: bool = False) -> None:
|
|
2270
|
+
if json_output:
|
|
2271
|
+
typer.echo(json.dumps({"items": sanitize_public_obj(rows)}, indent=2, sort_keys=True))
|
|
2272
|
+
return
|
|
1991
2273
|
if not rows:
|
|
1992
|
-
typer.echo('No
|
|
2274
|
+
typer.echo('No Experience Compilations matched.')
|
|
1993
2275
|
return
|
|
1994
|
-
for row in rows:
|
|
1995
|
-
typer.echo(_format_registry_row(row))
|
|
2276
|
+
for index, row in enumerate(rows, 1):
|
|
2277
|
+
typer.echo(_format_registry_row(row, index=index))
|
|
2278
|
+
if select:
|
|
2279
|
+
_select_registry_row(rows)
|
|
1996
2280
|
|
|
1997
2281
|
|
|
1998
2282
|
def _is_seed_registry_row(row: dict) -> bool:
|
|
@@ -2003,7 +2287,7 @@ def _format_count(value) -> str:
|
|
|
2003
2287
|
return str(int(value)) if isinstance(value, (int, float)) or str(value).isdigit() else "0"
|
|
2004
2288
|
|
|
2005
2289
|
|
|
2006
|
-
def _format_seed_registry_row(row: dict) -> str:
|
|
2290
|
+
def _format_seed_registry_row(row: dict, *, index: int | None = None) -> str:
|
|
2007
2291
|
bundle_id = row.get("bundle_id") or row.get("seed_task_id") or row.get("task_id")
|
|
2008
2292
|
status = row.get("task_status") or "ready_to_run"
|
|
2009
2293
|
run_status = row.get("run_status") or "not_run"
|
|
@@ -2025,34 +2309,46 @@ def _format_seed_registry_row(row: dict) -> str:
|
|
|
2025
2309
|
if row.get("revision_preference_pairs") is not None:
|
|
2026
2310
|
details.append(f"preferences: {_format_count(row.get('revision_preference_pairs'))}")
|
|
2027
2311
|
details.append(learning_label)
|
|
2028
|
-
|
|
2312
|
+
prefix = f"{index}. " if index is not None else ""
|
|
2313
|
+
return f"{prefix}{bundle_id}: {row.get('title')} [{' · '.join(details)}]"
|
|
2029
2314
|
|
|
2030
2315
|
|
|
2031
|
-
def _format_registry_row(row: dict) -> str:
|
|
2316
|
+
def _format_registry_row(row: dict, *, index: int | None = None) -> str:
|
|
2032
2317
|
if _is_seed_registry_row(row):
|
|
2033
|
-
return _format_seed_registry_row(row)
|
|
2318
|
+
return _format_seed_registry_row(row, index=index)
|
|
2034
2319
|
details = [
|
|
2035
2320
|
str(row.get("task_status") or row.get("run_status") or "status_unknown"),
|
|
2036
2321
|
]
|
|
2037
|
-
|
|
2038
|
-
|
|
2322
|
+
mode = row.get("apprenticeship_mode") or row.get("mentor_mode")
|
|
2323
|
+
if mode:
|
|
2324
|
+
details.append(f"apprenticeship_mode={apprenticeship_mode_display(normalize_apprenticeship_mode(str(mode)))}")
|
|
2039
2325
|
traces = row.get("traced_steps")
|
|
2040
2326
|
if traces is not None:
|
|
2041
2327
|
details.append(f"traces: {_format_count(traces)}")
|
|
2042
2328
|
artifacts = row.get("artifact_count")
|
|
2043
2329
|
if artifacts is not None:
|
|
2044
2330
|
details.append(f"artifacts: {_format_count(artifacts)}")
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2331
|
+
summary = str(row.get("summary") or "").strip()
|
|
2332
|
+
if len(summary) > 180:
|
|
2333
|
+
summary = summary[:177].rstrip() + "..."
|
|
2334
|
+
prefix = f"{index}. " if index is not None else ""
|
|
2335
|
+
return "\n".join([
|
|
2336
|
+
f"{prefix}{row.get('title') or _row_id(row)}",
|
|
2337
|
+
f" item_id: {_row_id(row)}",
|
|
2338
|
+
f" domain: {row.get('domain') or 'unknown'}",
|
|
2339
|
+
f" subdomain: {row.get('subdomain') or 'unknown'}",
|
|
2340
|
+
f" expected economic value: {row.get('expected_economic_value') or 'unknown'}",
|
|
2341
|
+
f" summary: {summary or 'No summary provided.'}",
|
|
2342
|
+
f" details: {' · '.join(details)}",
|
|
2343
|
+
" actions: 1. Export Full Experience Compilation 2. Install Runtime Training",
|
|
2344
|
+
])
|
|
2049
2345
|
|
|
2050
2346
|
|
|
2051
2347
|
def _safe_bundle_path(path: Path) -> Path:
|
|
2052
2348
|
bundle = path.expanduser()
|
|
2053
2349
|
manifest = bundle / "contribution_manifest.json"
|
|
2054
2350
|
if not manifest.exists():
|
|
2055
|
-
raise FileNotFoundError(f"
|
|
2351
|
+
raise FileNotFoundError(f"Experience Compilation manifest not found: {manifest}")
|
|
2056
2352
|
return bundle
|
|
2057
2353
|
|
|
2058
2354
|
|
|
@@ -2103,6 +2399,7 @@ def _bundle_submission_metadata(bundle: Path, package_name: str | None = None, p
|
|
|
2103
2399
|
"task_status": manifest.get("task_status"),
|
|
2104
2400
|
"run_status": manifest.get("run_status"),
|
|
2105
2401
|
"apprentice_agent": session.get("apprentice_agent"),
|
|
2402
|
+
"apprenticeship_mode": manifest.get("apprenticeship_mode") or session.get("apprenticeship_mode"),
|
|
2106
2403
|
"mentor_mode": manifest.get("mentor_mode"),
|
|
2107
2404
|
"mentor_model_provider": session.get("model_provider"),
|
|
2108
2405
|
"domains": manifest.get("domains") or [],
|
|
@@ -2150,7 +2447,7 @@ def _submission_summary(metadata: dict, bundle: Path, package_zip: Path | None =
|
|
|
2150
2447
|
f"Task Status: {metadata.get('task_status')}",
|
|
2151
2448
|
f"Run Status: {metadata.get('run_status')}",
|
|
2152
2449
|
f"Apprentice Agent: {metadata.get('apprentice_agent') or 'unknown'}",
|
|
2153
|
-
f"
|
|
2450
|
+
f"Apprenticeship Mode: {apprenticeship_mode_display(metadata.get('apprenticeship_mode') or metadata.get('mentor_mode'))}",
|
|
2154
2451
|
f"Mentor Model Provider: {metadata.get('mentor_model_provider') or 'none'}",
|
|
2155
2452
|
f"Attempts: {metadata.get('attempts')}",
|
|
2156
2453
|
f"Traced Steps: {metadata.get('traced_steps')}",
|
|
@@ -2238,12 +2535,12 @@ def _sanitize_submission_copy(root: Path) -> None:
|
|
|
2238
2535
|
path.write_text(sanitize_public_text(raw) or "")
|
|
2239
2536
|
|
|
2240
2537
|
|
|
2241
|
-
def _create_ecosystem_submission(bundle: Path) -> tuple[Path, Path, dict]:
|
|
2538
|
+
def _create_ecosystem_submission(bundle: Path, *, block_secret_like_values: bool = False) -> tuple[Path, Path, dict]:
|
|
2242
2539
|
bundle = _safe_bundle_path(bundle)
|
|
2243
2540
|
secret_hits = _bundle_secret_hits(bundle)
|
|
2244
|
-
if secret_hits:
|
|
2541
|
+
if block_secret_like_values and secret_hits:
|
|
2245
2542
|
raise RuntimeError(
|
|
2246
|
-
"Obvious secret-like values were found in the
|
|
2543
|
+
"Obvious secret-like values were found in the Experience Compilation. "
|
|
2247
2544
|
"Remove or regenerate the affected files before public contribution: "
|
|
2248
2545
|
+ ", ".join(secret_hits[:10])
|
|
2249
2546
|
)
|
|
@@ -2280,6 +2577,7 @@ def _ecosystem_contribution_record(
|
|
|
2280
2577
|
return {
|
|
2281
2578
|
"bundle_id": metadata.get("bundle_id"),
|
|
2282
2579
|
"auto_share_mode": auto_share_mode,
|
|
2580
|
+
"training_contribution_mode": auto_share_mode,
|
|
2283
2581
|
"contribution_created": contribution_created,
|
|
2284
2582
|
"contribution_url": contribution_url,
|
|
2285
2583
|
"contribution_method": contribution_method,
|
|
@@ -2301,10 +2599,15 @@ def _ecosystem_contribute_impl(
|
|
|
2301
2599
|
repo: str | None = None,
|
|
2302
2600
|
dry_run: bool = False,
|
|
2303
2601
|
auto_share_mode: str = "manual",
|
|
2602
|
+
storage: str | None = None,
|
|
2603
|
+
qa: bool = False,
|
|
2304
2604
|
) -> dict:
|
|
2305
2605
|
bundle = _safe_bundle_path(bundle_path)
|
|
2606
|
+
qa = qa or str(os.environ.get("AA_R2_QA_UPLOAD") or "").strip().lower() in {"1", "true", "yes", "on"}
|
|
2306
2607
|
submission_dir, package_zip, metadata = _create_ecosystem_submission(bundle)
|
|
2307
2608
|
target_repo = repo or _configured_ecosystem_repo()
|
|
2609
|
+
settings = get_settings()
|
|
2610
|
+
storage_backend = normalize_ecosystem_storage(storage or settings.ecosystem_storage_backend or DEFAULT_ECOSYSTEM_STORAGE_BACKEND)
|
|
2308
2611
|
issue_body = _gh_issue_body(metadata, submission_dir, package_zip)
|
|
2309
2612
|
result = {
|
|
2310
2613
|
"bundle": bundle,
|
|
@@ -2313,14 +2616,84 @@ def _ecosystem_contribute_impl(
|
|
|
2313
2616
|
"metadata": metadata,
|
|
2314
2617
|
"issue_body": issue_body,
|
|
2315
2618
|
"target_repo": target_repo,
|
|
2619
|
+
"storage_backend": storage_backend,
|
|
2620
|
+
"storage_ref": None,
|
|
2621
|
+
"ecosystem_item": None,
|
|
2622
|
+
"upload_response": None,
|
|
2316
2623
|
"contribution_created": False,
|
|
2317
2624
|
"contribution_url": None,
|
|
2318
2625
|
"contribution_method": "manual_package",
|
|
2319
2626
|
"skipped_reason": None,
|
|
2320
2627
|
"gh_error": None,
|
|
2321
2628
|
}
|
|
2629
|
+
|
|
2630
|
+
if storage_backend == "forsy-r2":
|
|
2631
|
+
identity = get_or_create_ecosystem_identity(settings)
|
|
2632
|
+
item = ecosystem_item_from_bundle(bundle, metadata, settings=settings, identity=identity, source="qa" if qa else "contribution")
|
|
2633
|
+
storage_ref = storage_ref_for_item(item, settings=settings, dry_run=dry_run)
|
|
2634
|
+
write_storage_ref(bundle, submission_dir, storage_ref)
|
|
2635
|
+
_zip_dir(submission_dir, package_zip)
|
|
2636
|
+
result["ecosystem_item"] = item
|
|
2637
|
+
result["storage_ref"] = storage_ref
|
|
2638
|
+
result["contribution_method"] = "r2_worker_ingestion" if not dry_run else "r2_worker_dry_run"
|
|
2639
|
+
if dry_run:
|
|
2640
|
+
result["skipped_reason"] = "dry run"
|
|
2641
|
+
else:
|
|
2642
|
+
api_url = r2_api_url(settings)
|
|
2643
|
+
if not api_url:
|
|
2644
|
+
result["skipped_reason"] = "R2 ingestion API is not configured"
|
|
2645
|
+
result["contribution_method"] = "skipped"
|
|
2646
|
+
else:
|
|
2647
|
+
response = upload_contribution_to_worker(
|
|
2648
|
+
api_url=api_url,
|
|
2649
|
+
package_zip=package_zip,
|
|
2650
|
+
ecosystem_item=item,
|
|
2651
|
+
manifest=read_json(bundle / "contribution_manifest.json"),
|
|
2652
|
+
storage_ref=storage_ref,
|
|
2653
|
+
qa=qa,
|
|
2654
|
+
)
|
|
2655
|
+
result["upload_response"] = response
|
|
2656
|
+
result["contribution_created"] = response.get("status") == "ok"
|
|
2657
|
+
result["contribution_url"] = (
|
|
2658
|
+
(response.get("storage") or {}).get("url")
|
|
2659
|
+
or storage_ref.get("url")
|
|
2660
|
+
or (api_url.rstrip("/") + f"/v1/ecosystem/items/{item.get('id')}")
|
|
2661
|
+
)
|
|
2662
|
+
response_storage = response.get("storage") if isinstance(response.get("storage"), dict) else {}
|
|
2663
|
+
if response_storage:
|
|
2664
|
+
item["storage"] = {**(item.get("storage") or {}), **response_storage}
|
|
2665
|
+
uploaded_ref = storage_ref_for_item(
|
|
2666
|
+
item,
|
|
2667
|
+
settings=settings,
|
|
2668
|
+
dry_run=False,
|
|
2669
|
+
uploaded_at=datetime.now(timezone.utc).isoformat(),
|
|
2670
|
+
url=result["contribution_url"],
|
|
2671
|
+
)
|
|
2672
|
+
write_storage_ref(bundle, submission_dir, uploaded_ref)
|
|
2673
|
+
_zip_dir(submission_dir, package_zip)
|
|
2674
|
+
result["storage_ref"] = uploaded_ref
|
|
2675
|
+
append_successful_share(
|
|
2676
|
+
item=item,
|
|
2677
|
+
bundle=bundle,
|
|
2678
|
+
storage_backend="forsy-r2",
|
|
2679
|
+
url=result["contribution_url"],
|
|
2680
|
+
sharing_mode=auto_share_mode,
|
|
2681
|
+
settings=settings,
|
|
2682
|
+
)
|
|
2683
|
+
record = _ecosystem_contribution_record(
|
|
2684
|
+
metadata=metadata,
|
|
2685
|
+
auto_share_mode=auto_share_mode,
|
|
2686
|
+
contribution_created=bool(result["contribution_created"]),
|
|
2687
|
+
contribution_url=result.get("contribution_url"),
|
|
2688
|
+
contribution_method=str(result["contribution_method"]),
|
|
2689
|
+
skipped_reason=result.get("skipped_reason"),
|
|
2690
|
+
)
|
|
2691
|
+
result["record"] = record
|
|
2692
|
+
_write_ecosystem_contribution_record(bundle, record)
|
|
2693
|
+
return result
|
|
2694
|
+
|
|
2322
2695
|
if target_repo and _gh_path() and _gh_authenticated() and not dry_run:
|
|
2323
|
-
title = f"
|
|
2696
|
+
title = f"Experience Compilation: {metadata.get('bundle_id')}"
|
|
2324
2697
|
cp = _run_gh(["issue", "create", "--repo", target_repo, "--title", title, "--body-file", str(issue_body)], timeout=60)
|
|
2325
2698
|
if cp.returncode == 0:
|
|
2326
2699
|
result["contribution_created"] = True
|
|
@@ -2356,12 +2729,12 @@ def _bundle_from_status(status: dict) -> Path | None:
|
|
|
2356
2729
|
|
|
2357
2730
|
|
|
2358
2731
|
def _print_auto_share_summary(metadata: dict) -> None:
|
|
2359
|
-
typer.echo("
|
|
2732
|
+
typer.echo("Experience Compilation summary")
|
|
2360
2733
|
typer.echo(f"bundle_id: {metadata.get('bundle_id')}")
|
|
2361
2734
|
typer.echo(f"title: {metadata.get('title')}")
|
|
2362
2735
|
typer.echo(f"task_status: {metadata.get('task_status')}")
|
|
2363
2736
|
typer.echo(f"Apprentice Agent: {metadata.get('apprentice_agent') or 'unknown'}")
|
|
2364
|
-
typer.echo(f"
|
|
2737
|
+
typer.echo(f"Apprenticeship Mode: {apprenticeship_mode_display(metadata.get('apprenticeship_mode') or metadata.get('mentor_mode'))}")
|
|
2365
2738
|
typer.echo(f"Mentor Model Provider: {metadata.get('mentor_model_provider') or 'none'}")
|
|
2366
2739
|
typer.echo(f"artifact_count: {metadata.get('artifact_count')}")
|
|
2367
2740
|
|
|
@@ -2381,28 +2754,32 @@ def _record_auto_share_skip(bundle: Path, *, mode: str, reason: str) -> None:
|
|
|
2381
2754
|
|
|
2382
2755
|
def _handle_ecosystem_auto_share(status: dict, *, quiet: bool = False, json_progress: bool = False) -> None:
|
|
2383
2756
|
settings = get_settings()
|
|
2384
|
-
mode = settings.
|
|
2385
|
-
if mode == "
|
|
2757
|
+
mode = settings.training_contribution_mode
|
|
2758
|
+
if mode == "private_internal_only":
|
|
2386
2759
|
return
|
|
2387
2760
|
bundle = _bundle_from_status(status)
|
|
2388
2761
|
if bundle is None:
|
|
2389
2762
|
return
|
|
2390
2763
|
emit = not quiet and not json_progress
|
|
2391
|
-
if mode == "
|
|
2392
|
-
|
|
2393
|
-
if
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
typer.echo("")
|
|
2398
|
-
typer.echo(f"Ecosystem Auto-Share skipped - {reason}")
|
|
2399
|
-
typer.echo("Ecosystem Auto-Share is configured for:")
|
|
2400
|
-
typer.echo(str(readiness.get("repo_url") or _ecosystem_repo_url()))
|
|
2401
|
-
if readiness.get("next_action"):
|
|
2764
|
+
if mode == "public_ecosystem":
|
|
2765
|
+
behavior = normalize_ecosystem_auto_share(settings.ecosystem_auto_share)
|
|
2766
|
+
if behavior == "manual":
|
|
2767
|
+
if not emit or not sys.stdin.isatty():
|
|
2768
|
+
_record_auto_share_skip(bundle, mode=mode, reason="manual sharing behavior; upload not confirmed")
|
|
2769
|
+
if emit:
|
|
2402
2770
|
typer.echo("")
|
|
2403
|
-
typer.echo("
|
|
2404
|
-
typer.echo(
|
|
2405
|
-
|
|
2771
|
+
typer.echo("Public Ecosystem upload: manual; no upload was made.")
|
|
2772
|
+
typer.echo(f"Experience Compilation: {bundle}")
|
|
2773
|
+
typer.echo(f"Retry: apprentice ecosystem contribute {bundle}")
|
|
2774
|
+
return
|
|
2775
|
+
typer.echo("")
|
|
2776
|
+
typer.echo("Share this Experience Compilation to the Forsy ecosystem? [y/N]")
|
|
2777
|
+
choice = typer.prompt("Share", default="N", show_default=False).strip().lower()
|
|
2778
|
+
if choice not in {"y", "yes"}:
|
|
2779
|
+
_record_auto_share_skip(bundle, mode=mode, reason="user chose not to share")
|
|
2780
|
+
typer.echo("Kept local package only.")
|
|
2781
|
+
typer.echo(f"Retry: apprentice ecosystem contribute {bundle}")
|
|
2782
|
+
return
|
|
2406
2783
|
try:
|
|
2407
2784
|
result = _ecosystem_contribute_impl(bundle, auto_share_mode=mode)
|
|
2408
2785
|
except Exception as exc:
|
|
@@ -2410,77 +2787,23 @@ def _handle_ecosystem_auto_share(status: dict, *, quiet: bool = False, json_prog
|
|
|
2410
2787
|
_record_auto_share_skip(bundle, mode=mode, reason=reason)
|
|
2411
2788
|
if emit:
|
|
2412
2789
|
typer.echo("")
|
|
2413
|
-
typer.echo("Ecosystem
|
|
2790
|
+
typer.echo("Public Ecosystem upload skipped - package could not be prepared")
|
|
2414
2791
|
typer.echo(f"Reason: {reason}")
|
|
2415
|
-
typer.echo(f"
|
|
2792
|
+
typer.echo(f"Experience Compilation: {bundle}")
|
|
2793
|
+
typer.echo(f"Retry: apprentice ecosystem contribute {bundle}")
|
|
2416
2794
|
return
|
|
2417
2795
|
if emit:
|
|
2418
2796
|
typer.echo("")
|
|
2419
2797
|
if result["contribution_created"]:
|
|
2420
|
-
typer.echo(
|
|
2798
|
+
typer.echo("Agent Training Contribution Mode: Public Ecosystem")
|
|
2799
|
+
typer.echo("Uploading Experience Compilation to Forsy ecosystem...")
|
|
2800
|
+
typer.echo(f"Uploaded: {result['contribution_url']}")
|
|
2421
2801
|
else:
|
|
2422
2802
|
reason = result.get("skipped_reason") or "public contribution was not created"
|
|
2423
|
-
typer.echo(f"Ecosystem
|
|
2424
|
-
typer.echo(f"
|
|
2803
|
+
typer.echo(f"Public Ecosystem upload skipped - {reason}")
|
|
2804
|
+
typer.echo(f"Experience Compilation: {bundle}")
|
|
2805
|
+
typer.echo(f"Retry: apprentice ecosystem contribute {bundle}")
|
|
2425
2806
|
return
|
|
2426
|
-
if mode == "ask":
|
|
2427
|
-
try:
|
|
2428
|
-
submission_dir, package_zip, metadata = _create_ecosystem_submission(bundle)
|
|
2429
|
-
_gh_issue_body(metadata, submission_dir, package_zip)
|
|
2430
|
-
except Exception as exc:
|
|
2431
|
-
reason = redact_secrets(str(exc))
|
|
2432
|
-
_record_auto_share_skip(bundle, mode=mode, reason=reason)
|
|
2433
|
-
if emit:
|
|
2434
|
-
typer.echo("")
|
|
2435
|
-
typer.echo("Ecosystem Auto-Share skipped - safety check failed")
|
|
2436
|
-
typer.echo(f"Reason: {reason}")
|
|
2437
|
-
typer.echo(f"Bundle: {bundle}")
|
|
2438
|
-
return
|
|
2439
|
-
if not emit:
|
|
2440
|
-
_record_auto_share_skip(bundle, mode=mode, reason="ask mode requires interactive output")
|
|
2441
|
-
return
|
|
2442
|
-
while True:
|
|
2443
|
-
typer.echo("")
|
|
2444
|
-
typer.echo("Share this Contribution Bundle to the public ecosystem now?")
|
|
2445
|
-
typer.echo("")
|
|
2446
|
-
typer.echo("1. Share now")
|
|
2447
|
-
typer.echo("2. Skip")
|
|
2448
|
-
typer.echo("3. Show submission summary")
|
|
2449
|
-
choice = typer.prompt("Default", default="skip").strip().lower()
|
|
2450
|
-
if choice in {"1", "share", "share now", "yes", "y"}:
|
|
2451
|
-
readiness = _ecosystem_auto_share_readiness()
|
|
2452
|
-
if not readiness["ready"]:
|
|
2453
|
-
reason = str(readiness.get("reason") or "not ready")
|
|
2454
|
-
_record_auto_share_skip(bundle, mode=mode, reason=reason)
|
|
2455
|
-
typer.echo(f"Ecosystem Auto-Share skipped - {reason}")
|
|
2456
|
-
typer.echo("Ecosystem Auto-Share is configured for:")
|
|
2457
|
-
typer.echo(str(readiness.get("repo_url") or _ecosystem_repo_url()))
|
|
2458
|
-
if readiness.get("next_action"):
|
|
2459
|
-
typer.echo("")
|
|
2460
|
-
typer.echo("Run:")
|
|
2461
|
-
typer.echo(str(readiness["next_action"]))
|
|
2462
|
-
return
|
|
2463
|
-
try:
|
|
2464
|
-
result = _ecosystem_contribute_impl(bundle, auto_share_mode=mode)
|
|
2465
|
-
except Exception as exc:
|
|
2466
|
-
reason = redact_secrets(str(exc))
|
|
2467
|
-
_record_auto_share_skip(bundle, mode=mode, reason=reason)
|
|
2468
|
-
typer.echo("Ecosystem Auto-Share skipped - safety check failed")
|
|
2469
|
-
typer.echo(f"Reason: {reason}")
|
|
2470
|
-
typer.echo(f"Bundle: {bundle}")
|
|
2471
|
-
return
|
|
2472
|
-
if result["contribution_created"]:
|
|
2473
|
-
typer.echo(f"Public ecosystem contribution created: {result['contribution_url']}")
|
|
2474
|
-
else:
|
|
2475
|
-
typer.echo(f"Ecosystem Auto-Share skipped - {result.get('skipped_reason') or 'public contribution was not created'}")
|
|
2476
|
-
typer.echo(f"Share later: apprentice ecosystem contribute {bundle}")
|
|
2477
|
-
return
|
|
2478
|
-
if choice in {"3", "summary", "show", "show submission summary"}:
|
|
2479
|
-
_print_auto_share_summary(metadata)
|
|
2480
|
-
continue
|
|
2481
|
-
_record_auto_share_skip(bundle, mode=mode, reason="user skipped")
|
|
2482
|
-
typer.echo(f"Share later: apprentice ecosystem contribute {bundle}")
|
|
2483
|
-
return
|
|
2484
2807
|
|
|
2485
2808
|
|
|
2486
2809
|
def _gh_issue_body(metadata: dict, submission_dir: Path, package_zip: Path) -> Path:
|
|
@@ -2519,11 +2842,12 @@ def learn_search(query: str | None = typer.Argument(None, help="Search ecosystem
|
|
|
2519
2842
|
|
|
2520
2843
|
@learn_app.command("create")
|
|
2521
2844
|
def learn_create(
|
|
2522
|
-
sources: list[str] = typer.Argument(..., help="Ecosystem ids or local
|
|
2845
|
+
sources: list[str] = typer.Argument(..., help="Ecosystem ids or local Experience Compilation paths."),
|
|
2523
2846
|
title: str | None = typer.Option(None, "--title", help="Optional Experience Pack title."),
|
|
2524
2847
|
replay: bool = typer.Option(False, "--replay", help="Run before/after replay after creating the pack."),
|
|
2525
2848
|
runner: str | None = typer.Option(None, "--runner", help="Advanced compatibility runner override for replay."),
|
|
2526
|
-
|
|
2849
|
+
apprenticeship_mode: str = typer.Option("expert-led", "--apprenticeship-mode", help="Apprenticeship Mode for replay."),
|
|
2850
|
+
mentor_mode: str | None = typer.Option(None, "--mentor-mode", help="Backward-compatible alias for --apprenticeship-mode.", hidden=True),
|
|
2527
2851
|
):
|
|
2528
2852
|
settings = get_settings()
|
|
2529
2853
|
resolved = [resolve_learning_source(source, settings) for source in sources]
|
|
@@ -2537,7 +2861,7 @@ def learn_create(
|
|
|
2537
2861
|
typer.echo(f"- apprentice learn preview {pack.get('pack_id')}")
|
|
2538
2862
|
typer.echo(f"- apprentice learn replay {pack.get('pack_id')}")
|
|
2539
2863
|
if replay:
|
|
2540
|
-
_learn_replay_impl(str(pack.get("pack_id")), runner=runner,
|
|
2864
|
+
_learn_replay_impl(str(pack.get("pack_id")), runner=runner, apprenticeship_mode=mentor_mode or apprenticeship_mode)
|
|
2541
2865
|
|
|
2542
2866
|
|
|
2543
2867
|
@learn_app.command("preview")
|
|
@@ -2565,8 +2889,8 @@ def learn_preview(pack_id: str = typer.Argument(..., help="Experience Pack id or
|
|
|
2565
2889
|
typer.echo(f"- {requirement}")
|
|
2566
2890
|
|
|
2567
2891
|
|
|
2568
|
-
def _learn_replay_impl(pack_id: str, *, runner: str | None,
|
|
2569
|
-
settings = _settings_for_run(
|
|
2892
|
+
def _learn_replay_impl(pack_id: str, *, runner: str | None, apprenticeship_mode: str) -> dict:
|
|
2893
|
+
settings = _settings_for_run(apprenticeship_mode=apprenticeship_mode, max_loops=1)
|
|
2570
2894
|
_ensure_apprentice_agent_ready(settings, runner, interactive=False)
|
|
2571
2895
|
pack_path, pack = load_pack(pack_id, settings)
|
|
2572
2896
|
instruction = replay_instruction_for_pack(pack, settings)
|
|
@@ -2621,9 +2945,10 @@ def _learn_replay_impl(pack_id: str, *, runner: str | None, mentor_mode: str) ->
|
|
|
2621
2945
|
def learn_replay(
|
|
2622
2946
|
pack_id: str = typer.Argument(..., help="Experience Pack id or path."),
|
|
2623
2947
|
runner: str | None = typer.Option(None, "--runner", help="Advanced compatibility runner override."),
|
|
2624
|
-
|
|
2948
|
+
apprenticeship_mode: str = typer.Option("expert-led", "--apprenticeship-mode", help="Apprenticeship Mode for replay."),
|
|
2949
|
+
mentor_mode: str | None = typer.Option(None, "--mentor-mode", help="Backward-compatible alias for --apprenticeship-mode.", hidden=True),
|
|
2625
2950
|
):
|
|
2626
|
-
_learn_replay_impl(pack_id, runner=runner,
|
|
2951
|
+
_learn_replay_impl(pack_id, runner=runner, apprenticeship_mode=mentor_mode or apprenticeship_mode)
|
|
2627
2952
|
|
|
2628
2953
|
|
|
2629
2954
|
@learn_app.command("keep")
|
|
@@ -2670,25 +2995,104 @@ def learn_status():
|
|
|
2670
2995
|
typer.echo(f"- {pack.get('pack_id')}: {pack.get('title')}")
|
|
2671
2996
|
|
|
2672
2997
|
|
|
2998
|
+
@learn_app.command("install")
|
|
2999
|
+
def learn_install(
|
|
3000
|
+
item_id: str=typer.Argument(..., help="Ecosystem item id or local Experience Compilation path."),
|
|
3001
|
+
dry_run: bool=typer.Option(False, "--dry-run", help="Show what would be installed without writing files."),
|
|
3002
|
+
agent_target: str=typer.Option("current", "--agent-target", help="current, codex, claude, cursor, opencode, openclaw, hermes, or custom."),
|
|
3003
|
+
registry: Path | None=typer.Option(None, "--registry", help="Public ecosystem index JSON path for offline/test use."),
|
|
3004
|
+
storage: str | None=typer.Option(None, "--storage", help="Storage backend: forsy-r2 or github."),
|
|
3005
|
+
):
|
|
3006
|
+
source_path = Path(item_id).expanduser()
|
|
3007
|
+
try:
|
|
3008
|
+
if source_path.exists():
|
|
3009
|
+
package = source_path
|
|
3010
|
+
source = str(source_path.name)
|
|
3011
|
+
else:
|
|
3012
|
+
package = _package_for_ecosystem_item(item_id, registry=registry, storage=storage)
|
|
3013
|
+
source = item_id
|
|
3014
|
+
result = install_runtime_training_from_bundle(source, package, settings=get_settings(), agent_target=agent_target, dry_run=dry_run)
|
|
3015
|
+
except Exception as exc:
|
|
3016
|
+
typer.echo(f"Could not install runtime training: {redact_secrets(str(exc))}")
|
|
3017
|
+
raise typer.Exit(1)
|
|
3018
|
+
if dry_run:
|
|
3019
|
+
typer.echo("Install Runtime Training dry run complete.")
|
|
3020
|
+
typer.echo(f"installed_skill_id: {result.get('installed_skill_id')}")
|
|
3021
|
+
typer.echo(f"target: {result.get('target')}")
|
|
3022
|
+
typer.echo("No files were installed.")
|
|
3023
|
+
return
|
|
3024
|
+
typer.echo("Install Runtime Training complete.")
|
|
3025
|
+
typer.echo(f"installed_skill_id: {result.get('installed_skill_id')}")
|
|
3026
|
+
typer.echo(f"enabled: {str(result.get('enabled')).lower()}")
|
|
3027
|
+
typer.echo("registry: local Agent Apprenticeship runtime")
|
|
3028
|
+
|
|
3029
|
+
|
|
3030
|
+
@learn_app.command("installed")
|
|
3031
|
+
def learn_installed(json_output: bool=typer.Option(False, "--json", help="Print installed runtime training as JSON.")):
|
|
3032
|
+
rows = list_installed_skills(get_settings())
|
|
3033
|
+
if json_output:
|
|
3034
|
+
typer.echo(json.dumps(rows, indent=2, sort_keys=True))
|
|
3035
|
+
return
|
|
3036
|
+
if not rows:
|
|
3037
|
+
typer.echo("Installed Runtime Training: none")
|
|
3038
|
+
return
|
|
3039
|
+
typer.echo("Installed Runtime Training")
|
|
3040
|
+
for row in rows:
|
|
3041
|
+
status = "enabled" if row.get("enabled", True) else "disabled"
|
|
3042
|
+
typer.echo(f"- {row.get('installed_skill_id')}: {row.get('title')} [{status}]")
|
|
3043
|
+
|
|
3044
|
+
|
|
3045
|
+
@learn_app.command("enable")
|
|
3046
|
+
def learn_enable(installed_skill_id: str=typer.Argument(..., help="Installed runtime training id.")):
|
|
3047
|
+
try:
|
|
3048
|
+
row = set_installed_skill_enabled(installed_skill_id, True, get_settings())
|
|
3049
|
+
except FileNotFoundError as exc:
|
|
3050
|
+
typer.echo(str(exc))
|
|
3051
|
+
raise typer.Exit(1)
|
|
3052
|
+
typer.echo("Runtime training enabled.")
|
|
3053
|
+
typer.echo(f"installed_skill_id: {row.get('installed_skill_id')}")
|
|
3054
|
+
|
|
3055
|
+
|
|
3056
|
+
@learn_app.command("disable")
|
|
3057
|
+
def learn_disable(installed_skill_id: str=typer.Argument(..., help="Installed runtime training id.")):
|
|
3058
|
+
try:
|
|
3059
|
+
row = set_installed_skill_enabled(installed_skill_id, False, get_settings())
|
|
3060
|
+
except FileNotFoundError as exc:
|
|
3061
|
+
typer.echo(str(exc))
|
|
3062
|
+
raise typer.Exit(1)
|
|
3063
|
+
typer.echo("Runtime training disabled.")
|
|
3064
|
+
typer.echo(f"installed_skill_id: {row.get('installed_skill_id')}")
|
|
3065
|
+
|
|
3066
|
+
|
|
3067
|
+
@learn_app.command("uninstall")
|
|
3068
|
+
def learn_uninstall(installed_skill_id: str=typer.Argument(..., help="Installed runtime training id.")):
|
|
3069
|
+
try:
|
|
3070
|
+
uninstall_installed_skill(installed_skill_id, get_settings())
|
|
3071
|
+
except FileNotFoundError as exc:
|
|
3072
|
+
typer.echo(str(exc))
|
|
3073
|
+
raise typer.Exit(1)
|
|
3074
|
+
typer.echo("Runtime training uninstalled.")
|
|
3075
|
+
typer.echo("registry: local Agent Apprenticeship runtime")
|
|
3076
|
+
|
|
3077
|
+
|
|
2673
3078
|
@ecosystem_app.command('guide')
|
|
2674
3079
|
def ecosystem_guide():
|
|
2675
3080
|
typer.echo('Agent Apprenticeship Ecosystem')
|
|
2676
3081
|
typer.echo('')
|
|
2677
|
-
typer.echo('The public ecosystem
|
|
2678
|
-
typer.echo('Run a task, then
|
|
3082
|
+
typer.echo('The public ecosystem stores shared Experience Compilations in Forsy Cloudflare R2.')
|
|
3083
|
+
typer.echo('Run a task, then share the generated Experience Compilation:')
|
|
2679
3084
|
typer.echo(' apprentice ecosystem contribute <bundle_path>')
|
|
2680
3085
|
typer.echo('')
|
|
2681
|
-
typer.echo('Configure
|
|
2682
|
-
typer.echo(
|
|
2683
|
-
typer.echo(' apprentice ecosystem configure --
|
|
2684
|
-
typer.echo(' apprentice ecosystem configure --repo-path <public-ecosystem-repo-checkout>')
|
|
3086
|
+
typer.echo('Configure ecosystem storage:')
|
|
3087
|
+
typer.echo(' apprentice ecosystem configure --storage forsy-r2')
|
|
3088
|
+
typer.echo(' apprentice ecosystem configure --r2-api-url <worker-url>')
|
|
2685
3089
|
typer.echo('')
|
|
2686
|
-
typer.echo('
|
|
2687
|
-
typer.echo(' apprentice ecosystem configure --
|
|
2688
|
-
typer.echo(' apprentice ecosystem configure --
|
|
3090
|
+
typer.echo('Agent Training Contribution Mode is Public Ecosystem by default.')
|
|
3091
|
+
typer.echo(' apprentice ecosystem configure --training-contribution public-ecosystem')
|
|
3092
|
+
typer.echo(' apprentice ecosystem configure --training-contribution private-internal-only')
|
|
2689
3093
|
typer.echo('')
|
|
2690
|
-
typer.echo('
|
|
2691
|
-
typer.echo('
|
|
3094
|
+
typer.echo('Legacy GitHub issue sharing remains available only with:')
|
|
3095
|
+
typer.echo(' apprentice ecosystem contribute <bundle_path> --storage github')
|
|
2692
3096
|
typer.echo(f'Community Slack: {SLACK_LINK}')
|
|
2693
3097
|
|
|
2694
3098
|
|
|
@@ -2696,28 +3100,40 @@ def ecosystem_guide():
|
|
|
2696
3100
|
def ecosystem_configure(
|
|
2697
3101
|
repo: str | None=typer.Option(None, '--repo', help='Public ecosystem repo in owner/name form.'),
|
|
2698
3102
|
repo_path: Path | None=typer.Option(None, '--repo-path', help='Public ecosystem repo path for a local checkout/export.'),
|
|
2699
|
-
|
|
3103
|
+
apprenticeship_mode: str | None=typer.Option(None, '--apprenticeship-mode', help='autonomous, expert-led, or org-custom.'),
|
|
3104
|
+
training_contribution: str | None=typer.Option(None, '--training-contribution', help='public-ecosystem or private-internal-only.'),
|
|
3105
|
+
sharing_behavior: str | None=typer.Option(None, '--sharing-behavior', help='Public Ecosystem behavior: manual or automatic.'),
|
|
3106
|
+
auto_share: str | None=typer.Option(None, '--auto-share', help='Deprecated alias for --training-contribution.', hidden=True),
|
|
3107
|
+
storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
|
|
3108
|
+
r2_api_url: str | None=typer.Option(None, '--r2-api-url', help='Forsy R2 Worker ingestion API URL.'),
|
|
3109
|
+
r2_bucket: str | None=typer.Option(None, '--r2-bucket', help='R2 bucket name.'),
|
|
3110
|
+
r2_index_prefix: str | None=typer.Option(None, '--r2-index-prefix', help='R2 index prefix.'),
|
|
3111
|
+
r2_contribution_prefix: str | None=typer.Option(None, '--r2-contribution-prefix', help='R2 Experience Compilation storage prefix.'),
|
|
3112
|
+
r2_seed_prefix: str | None=typer.Option(None, '--r2-seed-prefix', help='R2 seed dataset prefix.'),
|
|
2700
3113
|
):
|
|
2701
3114
|
settings = get_settings()
|
|
2702
|
-
if
|
|
3115
|
+
if all(v is None for v in [repo, repo_path, apprenticeship_mode, training_contribution, sharing_behavior, auto_share, storage, r2_api_url, r2_bucket, r2_index_prefix, r2_contribution_prefix, r2_seed_prefix]):
|
|
2703
3116
|
status = _ecosystem_status()
|
|
2704
3117
|
typer.echo("Public Ecosystem Configuration")
|
|
3118
|
+
typer.echo(f"Storage backend: {settings.ecosystem_storage_backend}")
|
|
3119
|
+
typer.echo(f"R2 ingestion API: {settings.r2_api_url or 'not configured'}")
|
|
3120
|
+
typer.echo(f"R2 bucket: {settings.r2_bucket}")
|
|
2705
3121
|
typer.echo(f"Current repo: {settings.ecosystem_repo or DEFAULT_PUBLIC_ECOSYSTEM_REPO}")
|
|
2706
3122
|
typer.echo(f"GitHub URL: {_ecosystem_repo_url(settings.ecosystem_repo)}")
|
|
2707
3123
|
typer.echo(f"Current public ecosystem repo path: {settings.ecosystem_repo_path or 'Not configured'}")
|
|
2708
3124
|
typer.echo(f"GitHub CLI installed: {_yes_no(status['gh_installed'])}")
|
|
2709
3125
|
typer.echo(f"GitHub CLI authenticated: {_yes_no(status['gh_authenticated'])}")
|
|
2710
|
-
typer.echo(f"Current
|
|
3126
|
+
typer.echo(f"Current Apprenticeship Mode: {apprenticeship_mode_display(settings.apprenticeship_mode)}")
|
|
3127
|
+
typer.echo(f"Current Agent Training Contribution Mode: {training_contribution_mode_display(settings.training_contribution_mode)}")
|
|
2711
3128
|
entered_repo = typer.prompt("Public ecosystem repo, owner/name, optional", default=settings.ecosystem_repo or DEFAULT_PUBLIC_ECOSYSTEM_REPO, show_default=False).strip()
|
|
2712
3129
|
if entered_repo:
|
|
2713
3130
|
repo = entered_repo
|
|
2714
3131
|
typer.echo("")
|
|
2715
|
-
typer.echo("Choose
|
|
2716
|
-
typer.echo("1.
|
|
2717
|
-
typer.echo("2.
|
|
2718
|
-
typer.
|
|
2719
|
-
|
|
2720
|
-
auto_share = {"1": "manual", "2": "ask", "3": "automatic"}.get(choice, choice)
|
|
3132
|
+
typer.echo("Choose Agent Training Contribution Mode:")
|
|
3133
|
+
typer.echo("1. Public Ecosystem")
|
|
3134
|
+
typer.echo("2. Private Internal Only")
|
|
3135
|
+
choice = typer.prompt("Default", default=settings.training_contribution_mode).strip().lower()
|
|
3136
|
+
training_contribution = {"1": "public_ecosystem", "2": "private_internal_only"}.get(choice, choice)
|
|
2721
3137
|
updates = {}
|
|
2722
3138
|
candidate_repo = repo or settings.ecosystem_repo or DEFAULT_PUBLIC_ECOSYSTEM_REPO
|
|
2723
3139
|
if repo is not None:
|
|
@@ -2734,23 +3150,31 @@ def ecosystem_configure(
|
|
|
2734
3150
|
typer.echo(f"Public ecosystem repo path does not look like an Agent Apprenticeship ecosystem repo: {expanded}")
|
|
2735
3151
|
raise typer.Exit(1)
|
|
2736
3152
|
updates["ecosystem_repo_path"] = expanded
|
|
3153
|
+
if apprenticeship_mode is not None:
|
|
3154
|
+
updates["apprenticeship_mode"] = _normalize_apprenticeship_mode(apprenticeship_mode)
|
|
3155
|
+
if training_contribution is not None:
|
|
3156
|
+
updates["training_contribution_mode"] = _normalize_training_contribution(training_contribution)
|
|
3157
|
+
if sharing_behavior is not None:
|
|
3158
|
+
updates["ecosystem_auto_share"] = _normalize_auto_share(sharing_behavior)
|
|
3159
|
+
if _normalize_auto_share(sharing_behavior) != "disabled":
|
|
3160
|
+
updates["training_contribution_mode"] = "public_ecosystem"
|
|
3161
|
+
else:
|
|
3162
|
+
updates["training_contribution_mode"] = "private_internal_only"
|
|
2737
3163
|
if auto_share is not None:
|
|
2738
|
-
|
|
2739
|
-
if
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
raise typer.Exit(1)
|
|
2753
|
-
updates["ecosystem_auto_share"] = mode
|
|
3164
|
+
updates["ecosystem_auto_share"] = _normalize_auto_share(auto_share)
|
|
3165
|
+
updates["training_contribution_mode"] = "public_ecosystem" if _normalize_auto_share(auto_share) != "disabled" else "private_internal_only"
|
|
3166
|
+
if storage is not None:
|
|
3167
|
+
updates["ecosystem_storage_backend"] = normalize_ecosystem_storage(storage)
|
|
3168
|
+
if r2_api_url is not None:
|
|
3169
|
+
updates["r2_api_url"] = r2_api_url.rstrip("/")
|
|
3170
|
+
if r2_bucket is not None:
|
|
3171
|
+
updates["r2_bucket"] = r2_bucket
|
|
3172
|
+
if r2_index_prefix is not None:
|
|
3173
|
+
updates["r2_index_prefix"] = r2_index_prefix.strip("/")
|
|
3174
|
+
if r2_contribution_prefix is not None:
|
|
3175
|
+
updates["r2_contribution_prefix"] = r2_contribution_prefix.strip("/")
|
|
3176
|
+
if r2_seed_prefix is not None:
|
|
3177
|
+
updates["r2_seed_prefix"] = r2_seed_prefix.strip("/")
|
|
2754
3178
|
if not updates:
|
|
2755
3179
|
typer.echo("No ecosystem settings changed.")
|
|
2756
3180
|
return
|
|
@@ -2759,8 +3183,21 @@ def ecosystem_configure(
|
|
|
2759
3183
|
typer.echo(f"Public ecosystem repo configured: {repo}")
|
|
2760
3184
|
if repo_path is not None:
|
|
2761
3185
|
typer.echo(f"Public ecosystem repo path configured: {Path(repo_path).expanduser()}")
|
|
2762
|
-
if
|
|
2763
|
-
typer.echo(f"
|
|
3186
|
+
if apprenticeship_mode is not None:
|
|
3187
|
+
typer.echo(f"Apprenticeship Mode: {apprenticeship_mode_display(updated.apprenticeship_mode)}")
|
|
3188
|
+
if training_contribution is not None or sharing_behavior is not None or auto_share is not None:
|
|
3189
|
+
typer.echo(f"Agent Training Contribution Mode: {training_contribution_mode_display(updated.training_contribution_mode)}")
|
|
3190
|
+
if updated.training_contribution_mode == "public_ecosystem":
|
|
3191
|
+
typer.echo(f"Public Ecosystem sharing behavior: {ecosystem_auto_share_display(updated.ecosystem_auto_share)}")
|
|
3192
|
+
if storage is not None:
|
|
3193
|
+
typer.echo(f"Storage backend: {updated.ecosystem_storage_backend}")
|
|
3194
|
+
if r2_api_url is not None:
|
|
3195
|
+
typer.echo(f"R2 ingestion API: {updated.r2_api_url or 'not configured'}")
|
|
3196
|
+
if any(v is not None for v in [r2_bucket, r2_index_prefix, r2_contribution_prefix, r2_seed_prefix]):
|
|
3197
|
+
typer.echo(f"R2 bucket: {updated.r2_bucket}")
|
|
3198
|
+
typer.echo(f"R2 index prefix: {updated.r2_index_prefix}")
|
|
3199
|
+
typer.echo(f"R2 Experience Compilation prefix: {updated.r2_contribution_prefix}")
|
|
3200
|
+
typer.echo(f"R2 seed prefix: {updated.r2_seed_prefix}")
|
|
2764
3201
|
typer.echo("Next: apprentice ecosystem status")
|
|
2765
3202
|
|
|
2766
3203
|
|
|
@@ -2769,32 +3206,116 @@ def ecosystem_status_command():
|
|
|
2769
3206
|
status = _ecosystem_status()
|
|
2770
3207
|
settings = get_settings()
|
|
2771
3208
|
typer.echo("Public Ecosystem Status")
|
|
2772
|
-
typer.echo(f"
|
|
3209
|
+
typer.echo(f"Storage backend: {status['storage_backend']}")
|
|
3210
|
+
typer.echo(f"R2 ingestion API: {status.get('r2_api_url') or 'not configured'}")
|
|
3211
|
+
typer.echo(f"R2 bucket: {status['r2_bucket']}")
|
|
3212
|
+
typer.echo(f"R2 index prefix: {status['r2_index_prefix']}")
|
|
3213
|
+
typer.echo(f"R2 Experience Compilation prefix: {status['r2_contribution_prefix']}")
|
|
3214
|
+
typer.echo(f"R2 seed prefix: {status['r2_seed_prefix']}")
|
|
3215
|
+
typer.echo(f"GitHub fallback repo: {status['repo'] or DEFAULT_PUBLIC_ECOSYSTEM_REPO}")
|
|
2773
3216
|
typer.echo(f"GitHub URL: {status['repo_url']}")
|
|
2774
3217
|
typer.echo(f"Public ecosystem repo path: {status.get('repo_path') or 'Not configured'}")
|
|
2775
3218
|
if status.get("repo_path"):
|
|
2776
3219
|
typer.echo(f"Public ecosystem repo path exists: {_yes_no(status.get('repo_path_exists'))}")
|
|
2777
3220
|
typer.echo(f"GitHub CLI installed: {_yes_no(status['gh_installed'])}")
|
|
2778
3221
|
typer.echo(f"GitHub CLI authenticated: {_yes_no(status['gh_authenticated'])}")
|
|
2779
|
-
typer.echo(f"
|
|
2780
|
-
typer.echo(f"
|
|
2781
|
-
|
|
3222
|
+
typer.echo(f"Apprenticeship Mode: {apprenticeship_mode_display(settings.apprenticeship_mode)}")
|
|
3223
|
+
typer.echo(f"Agent Training Contribution Mode: {training_contribution_mode_display(settings.training_contribution_mode)}")
|
|
3224
|
+
typer.echo(f"Local ecosystem user: {status.get('ecosystem_user_id')}")
|
|
3225
|
+
typer.echo(f"Shared contributions: {status.get('shared_count', 0)}")
|
|
3226
|
+
if settings.training_contribution_mode == "public_ecosystem":
|
|
2782
3227
|
readiness = _ecosystem_auto_share_readiness()
|
|
2783
|
-
typer.echo(f"Ecosystem
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
3228
|
+
typer.echo(f"Public Ecosystem upload status: {'Ready' if readiness['ready'] else 'Not ready - ' + str(readiness.get('reason') or 'not ready')}")
|
|
3229
|
+
|
|
3230
|
+
|
|
3231
|
+
@ecosystem_app.command('identity')
|
|
3232
|
+
def ecosystem_identity():
|
|
3233
|
+
settings = get_settings()
|
|
3234
|
+
identity = get_or_create_ecosystem_identity(settings)
|
|
3235
|
+
typer.echo("Local ecosystem identity")
|
|
3236
|
+
typer.echo(f"ecosystem_user_id: {identity.get('ecosystem_user_id')}")
|
|
3237
|
+
typer.echo(f"installation_id: {identity.get('installation_id')}")
|
|
3238
|
+
typer.echo(f"created_at: {identity.get('created_at')}")
|
|
3239
|
+
typer.echo(f"shared_contributions: {len(shared_history(settings))}")
|
|
3240
|
+
|
|
3241
|
+
|
|
3242
|
+
def _print_shared_rows(rows: list[dict[str, Any]]) -> None:
|
|
3243
|
+
if not rows:
|
|
3244
|
+
typer.echo("No shared Experience Compilations recorded yet.")
|
|
3245
|
+
return
|
|
3246
|
+
for row in rows:
|
|
3247
|
+
typer.echo(f"{row.get('item_id')}: {row.get('title')} [{row.get('storage_backend')} · {row.get('uploaded_at')}]")
|
|
3248
|
+
|
|
3249
|
+
|
|
3250
|
+
@ecosystem_shared_app.callback(invoke_without_command=True)
|
|
3251
|
+
def ecosystem_shared(
|
|
3252
|
+
ctx: typer.Context,
|
|
3253
|
+
json_output: bool = typer.Option(False, "--json", help="Print local shared Experience Compilation history as JSON."),
|
|
3254
|
+
):
|
|
3255
|
+
if ctx.invoked_subcommand is not None:
|
|
3256
|
+
return
|
|
3257
|
+
rows = shared_history(get_settings())
|
|
3258
|
+
if json_output:
|
|
3259
|
+
typer.echo(json.dumps(rows, indent=2, sort_keys=True))
|
|
3260
|
+
return
|
|
3261
|
+
_print_shared_rows(rows)
|
|
3262
|
+
|
|
3263
|
+
|
|
3264
|
+
def _local_shared_record(item_id: str) -> dict[str, Any]:
|
|
3265
|
+
for row in shared_history(get_settings()):
|
|
3266
|
+
if item_id in {str(row.get("item_id") or ""), str(row.get("package_id") or "")}:
|
|
3267
|
+
return row
|
|
3268
|
+
raise FileNotFoundError(f"Shared contribution not found locally: {item_id}")
|
|
3269
|
+
|
|
3270
|
+
|
|
3271
|
+
@ecosystem_shared_app.command("inspect")
|
|
3272
|
+
def ecosystem_shared_inspect(item_id: str = typer.Argument(...)):
|
|
3273
|
+
try:
|
|
3274
|
+
row = _local_shared_record(item_id)
|
|
3275
|
+
except FileNotFoundError as exc:
|
|
3276
|
+
typer.echo(str(exc))
|
|
3277
|
+
raise typer.Exit(1)
|
|
3278
|
+
typer.echo(f"item_id: {row.get('item_id')}")
|
|
3279
|
+
typer.echo(f"package_id: {row.get('package_id')}")
|
|
3280
|
+
typer.echo(f"title: {row.get('title')}")
|
|
3281
|
+
typer.echo(f"storage_backend: {row.get('storage_backend')}")
|
|
3282
|
+
typer.echo(f"url: {row.get('url')}")
|
|
3283
|
+
typer.echo(f"local_bundle_path: {row.get('local_bundle_path')}")
|
|
3284
|
+
|
|
3285
|
+
|
|
3286
|
+
@ecosystem_shared_app.command("pull")
|
|
3287
|
+
def ecosystem_shared_pull(item_id: str = typer.Argument(...)):
|
|
3288
|
+
try:
|
|
3289
|
+
row = _local_shared_record(item_id)
|
|
3290
|
+
except FileNotFoundError as exc:
|
|
3291
|
+
typer.echo(str(exc))
|
|
3292
|
+
raise typer.Exit(1)
|
|
3293
|
+
local = Path(str(row.get("local_bundle_path") or "")).expanduser()
|
|
3294
|
+
dest = _prepare_ecosystem_pull_destination(str(row.get("item_id") or item_id))
|
|
3295
|
+
if local.exists():
|
|
3296
|
+
if dest.exists():
|
|
3297
|
+
shutil.rmtree(dest)
|
|
3298
|
+
shutil.copytree(local, dest)
|
|
3299
|
+
typer.echo(f"pulled_bundle={dest}")
|
|
3300
|
+
return
|
|
3301
|
+
try:
|
|
3302
|
+
download_r2_package(str(row.get("item_id") or item_id), dest, get_settings())
|
|
3303
|
+
except Exception as exc:
|
|
3304
|
+
typer.echo(f"Could not pull shared Experience Compilation: {redact_secrets(str(exc))}")
|
|
3305
|
+
raise typer.Exit(1)
|
|
3306
|
+
typer.echo(f"pulled_bundle={dest}")
|
|
2788
3307
|
|
|
2789
3308
|
|
|
2790
3309
|
@ecosystem_app.command('contribute')
|
|
2791
3310
|
def ecosystem_contribute(
|
|
2792
|
-
bundle_path: Path=typer.Argument(..., help='
|
|
3311
|
+
bundle_path: Path=typer.Argument(..., help='Experience Compilation folder.'),
|
|
2793
3312
|
repo: str | None=typer.Option(None, '--repo', help='Public ecosystem repo in owner/name form.'),
|
|
2794
|
-
dry_run: bool=typer.Option(False, '--dry-run', help='Prepare
|
|
3313
|
+
dry_run: bool=typer.Option(False, '--dry-run', help='Prepare upload package without uploading.'),
|
|
3314
|
+
storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
|
|
3315
|
+
qa: bool=typer.Option(False, '--qa', help='Use QA storage path when supported.'),
|
|
2795
3316
|
):
|
|
2796
3317
|
try:
|
|
2797
|
-
result = _ecosystem_contribute_impl(bundle_path, repo=repo, dry_run=dry_run, auto_share_mode="manual")
|
|
3318
|
+
result = _ecosystem_contribute_impl(bundle_path, repo=repo, dry_run=dry_run, auto_share_mode="manual", storage=storage, qa=qa)
|
|
2798
3319
|
except Exception as exc:
|
|
2799
3320
|
typer.echo(f"Could not prepare public ecosystem contribution: {redact_secrets(str(exc))}")
|
|
2800
3321
|
raise typer.Exit(1)
|
|
@@ -2804,7 +3325,52 @@ def ecosystem_contribute(
|
|
|
2804
3325
|
metadata = result["metadata"]
|
|
2805
3326
|
target_repo = result["target_repo"]
|
|
2806
3327
|
issue_body = result["issue_body"]
|
|
2807
|
-
|
|
3328
|
+
if result.get("storage_backend") == "forsy-r2":
|
|
3329
|
+
item = result.get("ecosystem_item") or {}
|
|
3330
|
+
storage_ref = result.get("storage_ref") or {}
|
|
3331
|
+
if dry_run:
|
|
3332
|
+
typer.echo("Forsy ecosystem upload dry run complete.")
|
|
3333
|
+
typer.echo("No files were uploaded.")
|
|
3334
|
+
typer.echo("")
|
|
3335
|
+
typer.echo("Storage backend:")
|
|
3336
|
+
typer.echo("Cloudflare R2")
|
|
3337
|
+
typer.echo("")
|
|
3338
|
+
typer.echo("Ingestion API:")
|
|
3339
|
+
typer.echo(str(storage_ref.get("ingestion_api_url") or "not configured"))
|
|
3340
|
+
typer.echo("")
|
|
3341
|
+
typer.echo("Local ecosystem user:")
|
|
3342
|
+
typer.echo(str(storage_ref.get("ecosystem_user_id")))
|
|
3343
|
+
typer.echo("")
|
|
3344
|
+
typer.echo("Prepared package:")
|
|
3345
|
+
typer.echo(str(package_zip))
|
|
3346
|
+
typer.echo("")
|
|
3347
|
+
typer.echo("Storage reference:")
|
|
3348
|
+
typer.echo("ecosystem_submission/storage_ref.json")
|
|
3349
|
+
return
|
|
3350
|
+
if result["contribution_created"]:
|
|
3351
|
+
typer.echo("Experience Compilation uploaded to the Forsy ecosystem.")
|
|
3352
|
+
typer.echo("")
|
|
3353
|
+
typer.echo("Item:")
|
|
3354
|
+
typer.echo(str(item.get("id")))
|
|
3355
|
+
typer.echo("")
|
|
3356
|
+
typer.echo("Local ecosystem user:")
|
|
3357
|
+
typer.echo(str(storage_ref.get("ecosystem_user_id")))
|
|
3358
|
+
typer.echo("")
|
|
3359
|
+
typer.echo("Storage:")
|
|
3360
|
+
typer.echo(f"Cloudflare R2 / {(item.get('storage') or {}).get('path_prefix')}")
|
|
3361
|
+
typer.echo("")
|
|
3362
|
+
typer.echo("URL:")
|
|
3363
|
+
typer.echo(str(result["contribution_url"]))
|
|
3364
|
+
return
|
|
3365
|
+
typer.echo("Forsy ecosystem upload package prepared.")
|
|
3366
|
+
typer.echo(f"Reason upload did not run: {result.get('skipped_reason') or 'not uploaded'}")
|
|
3367
|
+
typer.echo(f"Prepared package: {package_zip}")
|
|
3368
|
+
typer.echo("Storage reference: ecosystem_submission/storage_ref.json")
|
|
3369
|
+
typer.echo("Retry after configuring the Worker URL:")
|
|
3370
|
+
typer.echo("apprentice ecosystem configure --r2-api-url <url>")
|
|
3371
|
+
return
|
|
3372
|
+
|
|
3373
|
+
typer.echo("Legacy GitHub ecosystem contribution prepared.")
|
|
2808
3374
|
typer.echo("")
|
|
2809
3375
|
typer.echo(f"Bundle ID: {metadata.get('bundle_id')}")
|
|
2810
3376
|
typer.echo(f"Title: {metadata.get('title')}")
|
|
@@ -2820,9 +3386,9 @@ def ecosystem_contribute(
|
|
|
2820
3386
|
typer.echo(str(result.get("gh_error") or ""))
|
|
2821
3387
|
elif target_repo and _gh_path() and _gh_authenticated() and dry_run:
|
|
2822
3388
|
typer.echo("Dry run: no GitHub issue was created.")
|
|
2823
|
-
typer.echo(f"Would run: gh issue create --repo {target_repo} --title \"
|
|
3389
|
+
typer.echo(f"Would run: gh issue create --repo {target_repo} --title \"Experience Compilation: {metadata.get('bundle_id')}\" --body-file GITHUB_ISSUE_BODY.md")
|
|
2824
3390
|
else:
|
|
2825
|
-
typer.echo("No
|
|
3391
|
+
typer.echo("No GitHub issue was created.")
|
|
2826
3392
|
typer.echo(f"Public ecosystem: {_ecosystem_repo_url(target_repo)}")
|
|
2827
3393
|
if not _gh_path():
|
|
2828
3394
|
typer.echo("GitHub CLI `gh` is not installed.")
|
|
@@ -2837,13 +3403,25 @@ def ecosystem_contribute(
|
|
|
2837
3403
|
|
|
2838
3404
|
|
|
2839
3405
|
@ecosystem_app.command('list')
|
|
2840
|
-
def ecosystem_list(
|
|
3406
|
+
def ecosystem_list(
|
|
3407
|
+
registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.'),
|
|
3408
|
+
storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
|
|
3409
|
+
mine: bool=typer.Option(False, '--mine', help='Show items shared by this local ecosystem user.'),
|
|
3410
|
+
domain: str | None=typer.Option(None, '--domain', help='Filter by domain.'),
|
|
3411
|
+
subdomain: str | None=typer.Option(None, '--subdomain', help='Filter by subdomain.'),
|
|
3412
|
+
tag: str | None=typer.Option(None, '--tag', help='Filter by tag.'),
|
|
3413
|
+
json_output: bool=typer.Option(False, '--json', help='Print matching Experience Compilations as JSON.'),
|
|
3414
|
+
):
|
|
2841
3415
|
try:
|
|
2842
|
-
rows=_load_registry(registry)
|
|
3416
|
+
rows=_load_registry(registry, storage=storage)
|
|
2843
3417
|
except FileNotFoundError as exc:
|
|
2844
3418
|
typer.echo(str(exc))
|
|
2845
3419
|
raise typer.Exit(1)
|
|
2846
|
-
|
|
3420
|
+
if mine:
|
|
3421
|
+
rows = [row for row in rows if _matches_registry_row(row, None, {"mine": True})]
|
|
3422
|
+
filters = {"domain": domain, "subdomain": subdomain, "tag": tag}
|
|
3423
|
+
rows = [row for row in rows if _matches_registry_row(row, None, filters)]
|
|
3424
|
+
_print_registry_rows(rows, json_output=json_output)
|
|
2847
3425
|
|
|
2848
3426
|
|
|
2849
3427
|
@ecosystem_app.command('search')
|
|
@@ -2851,21 +3429,27 @@ def ecosystem_search(
|
|
|
2851
3429
|
query_arg: str | None=typer.Argument(None, help='Free-text query.'),
|
|
2852
3430
|
query: str | None=typer.Option(None, '--query', help='Free-text query.'),
|
|
2853
3431
|
registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.'),
|
|
3432
|
+
storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
|
|
3433
|
+
mine: bool=typer.Option(False, '--mine', help='Search items shared by this local ecosystem user.'),
|
|
2854
3434
|
title: str | None=typer.Option(None, '--title'),
|
|
2855
3435
|
domain: str | None=typer.Option(None, '--domain'),
|
|
2856
3436
|
subdomain: str | None=typer.Option(None, '--subdomain'),
|
|
2857
|
-
|
|
3437
|
+
apprenticeship_mode: str | None=typer.Option(None, '--apprenticeship-mode', help='autonomous, expert-led, or org-custom.'),
|
|
3438
|
+
mentor_mode: str | None=typer.Option(None, '--mentor-mode', help='Backward-compatible alias for --apprenticeship-mode.', hidden=True),
|
|
2858
3439
|
task_status: str | None=typer.Option(None, '--task-status'),
|
|
2859
3440
|
run_status: str | None=typer.Option(None, '--run-status'),
|
|
2860
3441
|
min_traced_steps: int | None=typer.Option(None, '--min-traced-steps'),
|
|
2861
3442
|
max_traced_steps: int | None=typer.Option(None, '--max-traced-steps'),
|
|
2862
3443
|
expected_economic_value: str | None=typer.Option(None, '--expected-economic-value'),
|
|
3444
|
+
tag: str | None=typer.Option(None, '--tag'),
|
|
2863
3445
|
artifact_type: str | None=typer.Option(None, '--artifact-type'),
|
|
2864
3446
|
apprentice_agent: str | None=typer.Option(None, '--apprentice-agent'),
|
|
2865
3447
|
mentor_model_provider: str | None=typer.Option(None, '--mentor-model-provider'),
|
|
3448
|
+
json_output: bool=typer.Option(False, '--json', help='Print matching Experience Compilations as JSON.'),
|
|
3449
|
+
select: bool=typer.Option(False, '--select', help='Interactively choose a matching Experience Compilation and action.'),
|
|
2866
3450
|
):
|
|
2867
3451
|
try:
|
|
2868
|
-
rows=_load_registry(registry)
|
|
3452
|
+
rows=_load_registry(registry, storage=storage)
|
|
2869
3453
|
except FileNotFoundError as exc:
|
|
2870
3454
|
typer.echo(str(exc))
|
|
2871
3455
|
raise typer.Exit(1)
|
|
@@ -2874,14 +3458,25 @@ def ecosystem_search(
|
|
|
2874
3458
|
filters.pop('query_arg', None)
|
|
2875
3459
|
filters.pop('query', None)
|
|
2876
3460
|
filters.pop('registry', None)
|
|
3461
|
+
filters.pop('storage', None)
|
|
2877
3462
|
filters.pop('effective_query', None)
|
|
2878
|
-
|
|
3463
|
+
filters["mine"] = mine
|
|
3464
|
+
if mentor_mode and not apprenticeship_mode:
|
|
3465
|
+
filters["apprenticeship_mode"] = mentor_mode
|
|
3466
|
+
_print_registry_rows([row for row in rows if _matches_registry_row(row, effective_query, filters)], json_output=json_output, select=select)
|
|
2879
3467
|
|
|
2880
3468
|
|
|
2881
|
-
def _find_registry_row(bundle_id: str, registry: Path | None=None) -> dict:
|
|
2882
|
-
rows=_load_registry(registry)
|
|
3469
|
+
def _find_registry_row(bundle_id: str, registry: Path | None=None, storage: str | None = None) -> dict:
|
|
3470
|
+
rows=_load_registry(registry, storage=storage)
|
|
2883
3471
|
for row in rows:
|
|
2884
|
-
|
|
3472
|
+
ids = {
|
|
3473
|
+
str(row.get("id") or ""),
|
|
3474
|
+
str(row.get("bundle_id") or ""),
|
|
3475
|
+
str(row.get("package_id") or ""),
|
|
3476
|
+
str(row.get("seed_task_id") or ""),
|
|
3477
|
+
str(row.get("task_id") or ""),
|
|
3478
|
+
}
|
|
3479
|
+
if bundle_id in ids:
|
|
2885
3480
|
return row
|
|
2886
3481
|
raise FileNotFoundError(f'Bundle id not found in registry: {bundle_id}')
|
|
2887
3482
|
|
|
@@ -2901,16 +3496,113 @@ def _prepare_ecosystem_pull_destination(bundle_id: str) -> Path:
|
|
|
2901
3496
|
return dest
|
|
2902
3497
|
|
|
2903
3498
|
|
|
3499
|
+
def _locate_package_root(path: Path) -> Path:
|
|
3500
|
+
path = path.expanduser()
|
|
3501
|
+
if path.is_file():
|
|
3502
|
+
path = path.parent
|
|
3503
|
+
candidates = [path, path / "contribution_bundle", path / "package"]
|
|
3504
|
+
for candidate in candidates:
|
|
3505
|
+
if (candidate / "contribution_manifest.json").exists() or (candidate / "experience_compiler").exists():
|
|
3506
|
+
return candidate
|
|
3507
|
+
for manifest in path.rglob("contribution_manifest.json"):
|
|
3508
|
+
return manifest.parent
|
|
3509
|
+
return path
|
|
3510
|
+
|
|
3511
|
+
|
|
3512
|
+
def _copy_or_zip_package(src: Path, output: Path, *, item_id: str, source: str) -> Path:
|
|
3513
|
+
src = _locate_package_root(src)
|
|
3514
|
+
output = output.expanduser()
|
|
3515
|
+
manifest = {
|
|
3516
|
+
"export_type": "full_experience_compilation",
|
|
3517
|
+
"item_id": item_id,
|
|
3518
|
+
"source": source,
|
|
3519
|
+
"exported_at": datetime.now(timezone.utc).isoformat(),
|
|
3520
|
+
"package_root": "package",
|
|
3521
|
+
"includes": [
|
|
3522
|
+
"traces",
|
|
3523
|
+
"attempts",
|
|
3524
|
+
"artifacts",
|
|
3525
|
+
"outputs",
|
|
3526
|
+
"evaluation",
|
|
3527
|
+
"learning_signals",
|
|
3528
|
+
"experience_compiler",
|
|
3529
|
+
"skill_pack",
|
|
3530
|
+
"runtime_learning_package",
|
|
3531
|
+
"training_time_package",
|
|
3532
|
+
"environment",
|
|
3533
|
+
"manifests",
|
|
3534
|
+
],
|
|
3535
|
+
}
|
|
3536
|
+
if output.suffix == ".zip":
|
|
3537
|
+
staging = output.parent / f".{output.stem}-export"
|
|
3538
|
+
if staging.exists():
|
|
3539
|
+
shutil.rmtree(staging)
|
|
3540
|
+
(staging / "package").mkdir(parents=True)
|
|
3541
|
+
shutil.copytree(src, staging / "package", dirs_exist_ok=True)
|
|
3542
|
+
write_json(staging / "export_manifest.json", manifest)
|
|
3543
|
+
_zip_dir(staging, output)
|
|
3544
|
+
shutil.rmtree(staging)
|
|
3545
|
+
return output
|
|
3546
|
+
if output.exists():
|
|
3547
|
+
shutil.rmtree(output)
|
|
3548
|
+
output.mkdir(parents=True)
|
|
3549
|
+
shutil.copytree(src, output / "package", dirs_exist_ok=True)
|
|
3550
|
+
write_json(output / "export_manifest.json", manifest)
|
|
3551
|
+
return output
|
|
3552
|
+
|
|
3553
|
+
|
|
3554
|
+
def _package_for_ecosystem_item(item_id: str, *, registry: Path | None = None, storage: str | None = None) -> Path:
|
|
3555
|
+
direct = Path(item_id).expanduser()
|
|
3556
|
+
if direct.exists():
|
|
3557
|
+
return _locate_package_root(direct)
|
|
3558
|
+
local = get_settings().app_home / "ecosystem" / "bundles" / item_id
|
|
3559
|
+
if local.exists():
|
|
3560
|
+
return _locate_package_root(local)
|
|
3561
|
+
backend = normalize_ecosystem_storage(storage or get_settings().ecosystem_storage_backend)
|
|
3562
|
+
if backend == "forsy-r2" and registry is None:
|
|
3563
|
+
dest = _prepare_ecosystem_pull_destination(item_id)
|
|
3564
|
+
download_r2_package(item_id, dest, get_settings())
|
|
3565
|
+
return _locate_package_root(dest)
|
|
3566
|
+
row = _find_registry_row(item_id, registry, storage=storage)
|
|
3567
|
+
src = row.get("local_bundle_path") or row.get("path") or row.get("bundle_path") or row.get("bundle_path_or_url")
|
|
3568
|
+
if src and Path(str(src)).expanduser().exists():
|
|
3569
|
+
return _locate_package_root(Path(str(src)).expanduser())
|
|
3570
|
+
raise FileNotFoundError(f"No full package is available for ecosystem item: {item_id}")
|
|
3571
|
+
|
|
3572
|
+
|
|
2904
3573
|
@ecosystem_app.command('pull')
|
|
2905
3574
|
def ecosystem_pull(
|
|
2906
3575
|
bundle_id: str=typer.Argument(...),
|
|
2907
3576
|
registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.'),
|
|
3577
|
+
storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
|
|
3578
|
+
full: bool=typer.Option(False, '--full', help='Export the Full Experience Compilation after pulling.'),
|
|
3579
|
+
runtime: bool=typer.Option(False, '--runtime', help='Pull runtime training subset for install.'),
|
|
3580
|
+
install: bool=typer.Option(False, '--install', help='Install runtime training after pulling.'),
|
|
3581
|
+
mode: str | None=typer.Option(None, '--mode', help='full or runtime.'),
|
|
2908
3582
|
):
|
|
2909
3583
|
try:
|
|
2910
|
-
row=_find_registry_row(bundle_id, registry)
|
|
3584
|
+
row=_find_registry_row(bundle_id, registry, storage=storage)
|
|
2911
3585
|
except FileNotFoundError as exc:
|
|
2912
3586
|
typer.echo(str(exc))
|
|
2913
3587
|
raise typer.Exit(1)
|
|
3588
|
+
backend = normalize_ecosystem_storage(storage or get_settings().ecosystem_storage_backend)
|
|
3589
|
+
if backend == "forsy-r2" and not registry:
|
|
3590
|
+
dest=_prepare_ecosystem_pull_destination(_row_id(row) or bundle_id)
|
|
3591
|
+
try:
|
|
3592
|
+
download_r2_package(str(row.get("id") or bundle_id), dest, get_settings())
|
|
3593
|
+
except Exception as exc:
|
|
3594
|
+
typer.echo(f"Could not pull ecosystem item from R2: {redact_secrets(str(exc))}")
|
|
3595
|
+
typer.echo("Fallback: apprentice ecosystem pull " + bundle_id + " --storage github")
|
|
3596
|
+
raise typer.Exit(1)
|
|
3597
|
+
typer.echo(f'pulled_bundle={dest}')
|
|
3598
|
+
if full or mode == "full":
|
|
3599
|
+
export_path = _copy_or_zip_package(dest, get_settings().app_home / "ecosystem" / "exports" / (_row_id(row) or bundle_id), item_id=_row_id(row) or bundle_id, source="r2_pull")
|
|
3600
|
+
typer.echo(f"full_experience_compilation={export_path}")
|
|
3601
|
+
if install or runtime or mode == "runtime":
|
|
3602
|
+
manifest = install_runtime_training_from_bundle(_row_id(row) or bundle_id, dest, settings=get_settings())
|
|
3603
|
+
typer.echo("Runtime training installed.")
|
|
3604
|
+
typer.echo(f"installed_skill_id: {manifest.get('installed_skill_id')}")
|
|
3605
|
+
return
|
|
2914
3606
|
src=row.get('local_bundle_path') or row.get('path') or row.get('bundle_path') or row.get('bundle_path_or_url')
|
|
2915
3607
|
if not src:
|
|
2916
3608
|
task_path = row.get("task_path") or row.get("task_packet_path")
|
|
@@ -2978,6 +3670,39 @@ def ecosystem_pull(
|
|
|
2978
3670
|
typer.echo('Next action: set AA_HOME to a writable directory and try again.')
|
|
2979
3671
|
raise typer.Exit(1)
|
|
2980
3672
|
typer.echo(f'pulled_bundle={dest}')
|
|
3673
|
+
if full or mode == "full":
|
|
3674
|
+
export_path = _copy_or_zip_package(dest, get_settings().app_home / "ecosystem" / "exports" / bundle_id, item_id=bundle_id, source="local_pull")
|
|
3675
|
+
typer.echo(f"full_experience_compilation={export_path}")
|
|
3676
|
+
if install or runtime or mode == "runtime":
|
|
3677
|
+
try:
|
|
3678
|
+
manifest = install_runtime_training_from_bundle(bundle_id, dest, settings=get_settings())
|
|
3679
|
+
except Exception as exc:
|
|
3680
|
+
typer.echo(f"Could not install runtime training: {redact_secrets(str(exc))}")
|
|
3681
|
+
raise typer.Exit(1)
|
|
3682
|
+
typer.echo("Runtime training installed.")
|
|
3683
|
+
typer.echo(f"installed_skill_id: {manifest.get('installed_skill_id')}")
|
|
3684
|
+
|
|
3685
|
+
|
|
3686
|
+
@ecosystem_app.command('export')
|
|
3687
|
+
def ecosystem_export(
|
|
3688
|
+
item_id: str=typer.Argument(..., help='Ecosystem item id.'),
|
|
3689
|
+
output: Path=typer.Option(..., '--output', '-o', help='Output folder or .zip path.'),
|
|
3690
|
+
full: bool=typer.Option(False, '--full', help='Export the Full Experience Compilation.'),
|
|
3691
|
+
mode: str | None=typer.Option(None, '--mode', help='Export mode: full.'),
|
|
3692
|
+
registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.'),
|
|
3693
|
+
storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
|
|
3694
|
+
):
|
|
3695
|
+
if not full and mode not in {None, "full"}:
|
|
3696
|
+
raise typer.BadParameter("Only Full Experience Compilation export is supported for this command.")
|
|
3697
|
+
try:
|
|
3698
|
+
package = _package_for_ecosystem_item(item_id, registry=registry, storage=storage)
|
|
3699
|
+
exported = _copy_or_zip_package(package, output, item_id=item_id, source="ecosystem_export")
|
|
3700
|
+
except Exception as exc:
|
|
3701
|
+
typer.echo(f"Could not export Full Experience Compilation: {redact_secrets(str(exc))}")
|
|
3702
|
+
raise typer.Exit(1)
|
|
3703
|
+
typer.echo("Export Full Experience Compilation complete.")
|
|
3704
|
+
typer.echo(f"item_id: {item_id}")
|
|
3705
|
+
typer.echo(f"output: {exported}")
|
|
2981
3706
|
|
|
2982
3707
|
|
|
2983
3708
|
def _summarize_bundle_manifest(manifest: dict) -> str:
|
|
@@ -2985,14 +3710,16 @@ def _summarize_bundle_manifest(manifest: dict) -> str:
|
|
|
2985
3710
|
'bundle_id','title','attempts','traced_steps','process_supervision_rows',
|
|
2986
3711
|
'reward_modeling_rows','revision_preference_pairs','domains','subdomains',
|
|
2987
3712
|
'agent_apprentice_role','expected_economic_value',
|
|
2988
|
-
'expected_economic_value_for_agent_apprentice','
|
|
3713
|
+
'expected_economic_value_for_agent_apprentice','task_status','run_status'
|
|
2989
3714
|
]
|
|
2990
3715
|
lines = []
|
|
2991
3716
|
for key in keys:
|
|
2992
3717
|
if key not in manifest:
|
|
2993
3718
|
continue
|
|
2994
|
-
|
|
2995
|
-
|
|
3719
|
+
lines.append(f"{key}: {manifest.get(key)}")
|
|
3720
|
+
mode = manifest.get("apprenticeship_mode") or manifest.get("mentor_mode")
|
|
3721
|
+
if mode:
|
|
3722
|
+
lines.append(f"apprenticeship_mode: {apprenticeship_mode_display(normalize_apprenticeship_mode(str(mode)))}")
|
|
2996
3723
|
return "\n".join(lines)
|
|
2997
3724
|
|
|
2998
3725
|
|
|
@@ -3017,21 +3744,23 @@ def _summarize_seed_registry_row(row: dict) -> str:
|
|
|
3017
3744
|
lines.append(f"domains: {row.get('domains')}")
|
|
3018
3745
|
if row.get("subdomains"):
|
|
3019
3746
|
lines.append(f"subdomains: {row.get('subdomains')}")
|
|
3020
|
-
lines.append("note: this is a seed task, not a completed
|
|
3747
|
+
lines.append("note: this is a seed task, not a completed Experience Compilation")
|
|
3021
3748
|
return "\n".join(lines)
|
|
3022
3749
|
|
|
3023
3750
|
|
|
3024
3751
|
def _summarize_contribution_registry_row(row: dict) -> str:
|
|
3752
|
+
item_id = _row_id(row)
|
|
3753
|
+
compiler = row.get("experience_compiler") or {}
|
|
3754
|
+
available = row.get("available_outputs") or {}
|
|
3025
3755
|
lines = [
|
|
3026
|
-
"kind:
|
|
3027
|
-
f"
|
|
3756
|
+
f"kind: {row.get('kind') or 'agent_experience_package'}",
|
|
3757
|
+
f"item_id: {item_id}",
|
|
3028
3758
|
f"title: {row.get('title')}",
|
|
3029
3759
|
f"task_status: {row.get('task_status') or 'unknown'}",
|
|
3030
3760
|
f"run_status: {row.get('run_status') or 'unknown'}",
|
|
3031
3761
|
]
|
|
3032
3762
|
for key in [
|
|
3033
3763
|
"apprentice_agent",
|
|
3034
|
-
"mentor_mode",
|
|
3035
3764
|
"mentor_model_provider",
|
|
3036
3765
|
"attempts",
|
|
3037
3766
|
"traced_steps",
|
|
@@ -3043,26 +3772,119 @@ def _summarize_contribution_registry_row(row: dict) -> str:
|
|
|
3043
3772
|
"expected_economic_value_for_agent_apprentice",
|
|
3044
3773
|
]:
|
|
3045
3774
|
if row.get(key) is not None:
|
|
3046
|
-
|
|
3047
|
-
|
|
3775
|
+
lines.append(f"{key}: {row.get(key)}")
|
|
3776
|
+
mode = row.get("apprenticeship_mode") or row.get("mentor_mode")
|
|
3777
|
+
if mode:
|
|
3778
|
+
lines.append(f"apprenticeship_mode: {apprenticeship_mode_display(normalize_apprenticeship_mode(str(mode)))}")
|
|
3048
3779
|
if row.get("domains"):
|
|
3049
3780
|
lines.append(f"domains: {row.get('domains')}")
|
|
3050
3781
|
if row.get("subdomains"):
|
|
3051
3782
|
lines.append(f"subdomains: {row.get('subdomains')}")
|
|
3783
|
+
lines.extend(
|
|
3784
|
+
[
|
|
3785
|
+
f"has Full Experience Compilation: {'yes' if available.get('full_experience_compilation', available.get('full_training_package', row.get('kind') == 'agent_experience_package')) else 'no'}",
|
|
3786
|
+
f"has runtime training: {'yes' if available.get('runtime_training', row.get('kind') == 'agent_experience_package') else 'no'}",
|
|
3787
|
+
f"skill title: {compiler.get('skill_title') or row.get('title') or 'unknown'}",
|
|
3788
|
+
f"skill summary: {compiler.get('skill_summary') or compiler.get('runtime_training_summary') or 'available when Experience Compiler outputs are present'}",
|
|
3789
|
+
f"environment domain: {compiler.get('environment_domain') or row.get('environment_domain') or row.get('domain') or 'unknown'}",
|
|
3790
|
+
f"Experience Compiler status: {compiler.get('status') or row.get('experience_compiler_status') or row.get('tdo_status') or 'unknown'}",
|
|
3791
|
+
]
|
|
3792
|
+
)
|
|
3793
|
+
installed = [
|
|
3794
|
+
skill for skill in list_installed_skills(get_settings())
|
|
3795
|
+
if item_id in {str(skill.get("source_item_id") or ""), str(skill.get("source_package_id") or "")}
|
|
3796
|
+
]
|
|
3797
|
+
lines.append(f"local installed status: {'installed' if installed else 'not installed'}")
|
|
3798
|
+
lines.extend(
|
|
3799
|
+
[
|
|
3800
|
+
"",
|
|
3801
|
+
"Available actions:",
|
|
3802
|
+
"",
|
|
3803
|
+
"1. Export Full Experience Compilation",
|
|
3804
|
+
" Use this when you want the complete traces, artifacts, training data, evaluation data, runtime learning, and generated data packages for research/training/evaluation pipelines.",
|
|
3805
|
+
" Command:",
|
|
3806
|
+
f" apprentice ecosystem export {item_id} --full --output <path>",
|
|
3807
|
+
"",
|
|
3808
|
+
"2. Install Runtime Training",
|
|
3809
|
+
" Use this when you want your Apprentice Agent to use the compiled skill/runtime learning in future runs.",
|
|
3810
|
+
" Command:",
|
|
3811
|
+
f" apprentice learn install {item_id}",
|
|
3812
|
+
]
|
|
3813
|
+
)
|
|
3052
3814
|
return "\n".join(lines)
|
|
3053
3815
|
|
|
3054
3816
|
|
|
3817
|
+
def _local_package_registry_row(path: Path) -> dict:
|
|
3818
|
+
package = _locate_package_root(path)
|
|
3819
|
+
manifest = {}
|
|
3820
|
+
for candidate in [
|
|
3821
|
+
package / "contribution_manifest.json",
|
|
3822
|
+
package / "task_manifest.json",
|
|
3823
|
+
package / "package_manifest.json",
|
|
3824
|
+
package / "manifest.json",
|
|
3825
|
+
]:
|
|
3826
|
+
if candidate.exists():
|
|
3827
|
+
try:
|
|
3828
|
+
manifest = read_json(candidate)
|
|
3829
|
+
break
|
|
3830
|
+
except Exception:
|
|
3831
|
+
manifest = {}
|
|
3832
|
+
compiler = package / "experience_compiler"
|
|
3833
|
+
canonical = compiler / "skill_pack" / "canonical_skill.json"
|
|
3834
|
+
skill = read_json(canonical) if canonical.exists() else {}
|
|
3835
|
+
compiler_report = read_json(compiler / "compiler_report.json") if (compiler / "compiler_report.json").exists() else {}
|
|
3836
|
+
item_id = str(manifest.get("item_id") or manifest.get("package_id") or package.name)
|
|
3837
|
+
return {
|
|
3838
|
+
"id": item_id,
|
|
3839
|
+
"kind": "agent_experience_package",
|
|
3840
|
+
"title": skill.get("title") or manifest.get("title") or package.name,
|
|
3841
|
+
"summary": skill.get("summary") or compiler_report.get("description"),
|
|
3842
|
+
"domain": manifest.get("domain"),
|
|
3843
|
+
"subdomain": manifest.get("subdomain"),
|
|
3844
|
+
"package_id": manifest.get("package_id") or item_id,
|
|
3845
|
+
"task_id": manifest.get("task_id"),
|
|
3846
|
+
"local_bundle_path": str(package),
|
|
3847
|
+
"experience_compiler_status": compiler_report.get("compiler_status") or compiler_report.get("tdo_status") or ("completed" if compiler.exists() else "unknown"),
|
|
3848
|
+
"available_outputs": {
|
|
3849
|
+
"full_experience_compilation": True,
|
|
3850
|
+
"full_training_package": True,
|
|
3851
|
+
"runtime_training": bool((compiler / "skill_pack" / "skill.md").exists()),
|
|
3852
|
+
"skill_pack": (compiler / "skill_pack").exists(),
|
|
3853
|
+
"training_time_package": (compiler / "training_time_package").exists(),
|
|
3854
|
+
"eval_package": (compiler / "eval_package").exists(),
|
|
3855
|
+
"environment_package": (compiler / "environment").exists(),
|
|
3856
|
+
},
|
|
3857
|
+
"experience_compiler": {
|
|
3858
|
+
"enabled": compiler.exists(),
|
|
3859
|
+
"status": compiler_report.get("compiler_status") or compiler_report.get("tdo_status") or ("completed" if compiler.exists() else "unknown"),
|
|
3860
|
+
"skill_title": skill.get("title") or manifest.get("title") or package.name,
|
|
3861
|
+
"skill_summary": skill.get("summary") or compiler_report.get("description"),
|
|
3862
|
+
"runtime_training_summary": skill.get("summary") or "Runtime training available from local Experience Compiler outputs.",
|
|
3863
|
+
"training_data_counts": compiler_report.get("rows_accepted_by_type") or {},
|
|
3864
|
+
"environment_domain": manifest.get("environment_domain") or manifest.get("domain"),
|
|
3865
|
+
},
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
|
|
3055
3869
|
@ecosystem_app.command('inspect')
|
|
3056
3870
|
def ecosystem_inspect(
|
|
3057
3871
|
bundle_id: str=typer.Argument(...),
|
|
3058
3872
|
registry: Path | None=typer.Option(None, '--registry', help='Public ecosystem index JSON path for offline/test use.'),
|
|
3873
|
+
storage: str | None=typer.Option(None, '--storage', help='Storage backend: forsy-r2 or github.'),
|
|
3059
3874
|
):
|
|
3875
|
+
local_arg = Path(bundle_id).expanduser()
|
|
3876
|
+
if local_arg.exists():
|
|
3877
|
+
typer.echo(_summarize_contribution_registry_row(_local_package_registry_row(local_arg)))
|
|
3878
|
+
return
|
|
3060
3879
|
bundle_path=get_settings().app_home / 'ecosystem' / 'bundles' / bundle_id
|
|
3061
3880
|
if bundle_path.exists() and (bundle_path/'contribution_manifest.json').exists():
|
|
3062
3881
|
typer.echo(_summarize_bundle_manifest(read_json(bundle_path/'contribution_manifest.json')))
|
|
3063
3882
|
return
|
|
3064
3883
|
try:
|
|
3065
|
-
|
|
3884
|
+
if normalize_ecosystem_storage(storage or get_settings().ecosystem_storage_backend) == "forsy-r2" and registry is None:
|
|
3885
|
+
row=fetch_r2_item(bundle_id, get_settings())
|
|
3886
|
+
else:
|
|
3887
|
+
row=_find_registry_row(bundle_id, registry, storage=storage)
|
|
3066
3888
|
except FileNotFoundError as exc:
|
|
3067
3889
|
typer.echo(str(exc))
|
|
3068
3890
|
raise typer.Exit(1)
|
|
@@ -3073,10 +3895,12 @@ def ecosystem_inspect(
|
|
|
3073
3895
|
|
|
3074
3896
|
|
|
3075
3897
|
@bundle_app.command('inspect')
|
|
3076
|
-
def bundle_inspect(path: Path=typer.Argument(..., help='
|
|
3898
|
+
def bundle_inspect(path: Path=typer.Argument(..., help='Experience Compilation folder.')):
|
|
3899
|
+
from .loop_review import summarize_loop_reviews
|
|
3900
|
+
|
|
3077
3901
|
manifest=path/'contribution_manifest.json'
|
|
3078
3902
|
if not manifest.exists():
|
|
3079
|
-
typer.echo(f'
|
|
3903
|
+
typer.echo(f'Experience Compilation manifest not found: {manifest}')
|
|
3080
3904
|
raise typer.Exit(1)
|
|
3081
3905
|
manifest_data=read_json(manifest)
|
|
3082
3906
|
artifacts=_artifact_index(path)
|
|
@@ -3092,6 +3916,15 @@ def bundle_inspect(path: Path=typer.Argument(..., help='Contribution Bundle fold
|
|
|
3092
3916
|
typer.echo(f"follow_up_count: {len(followups)}")
|
|
3093
3917
|
if checkpoints:
|
|
3094
3918
|
typer.echo(f"mentor_checkpoint_count: {len(checkpoints)}")
|
|
3919
|
+
loop_summary = summarize_loop_reviews(path)
|
|
3920
|
+
if loop_summary.get("has_loop_review_packets"):
|
|
3921
|
+
typer.echo(f"loop_review_packets: {loop_summary.get('loop_iteration_count')}")
|
|
3922
|
+
typer.echo(f"loop_final_verdict: {loop_summary.get('final_loop_verdict') or 'unknown'}")
|
|
3923
|
+
reviewers = ", ".join(loop_summary.get("reviewer_types") or []) or "unknown"
|
|
3924
|
+
typer.echo(f"loop_reviewer_types: {reviewers}")
|
|
3925
|
+
refs = loop_summary.get("loop_review_refs") or []
|
|
3926
|
+
if refs:
|
|
3927
|
+
typer.echo(f"latest_review_packet: {refs[-1]}")
|
|
3095
3928
|
top=[row.get("package_relative_path") or row.get("artifact_ref") for row in artifacts[:5]]
|
|
3096
3929
|
if top:
|
|
3097
3930
|
typer.echo("top_artifacts:")
|
|
@@ -3102,14 +3935,16 @@ def bundle_inspect(path: Path=typer.Argument(..., help='Contribution Bundle fold
|
|
|
3102
3935
|
|
|
3103
3936
|
|
|
3104
3937
|
@bundle_app.command('check')
|
|
3105
|
-
def bundle_check(path: Path=typer.Argument(..., help='
|
|
3938
|
+
def bundle_check(path: Path=typer.Argument(..., help='Experience Compilation folder.')):
|
|
3939
|
+
from .artifact_resolver import artifact_ref_candidates, artifact_ref_resolves, normalize_artifact_ref
|
|
3940
|
+
|
|
3106
3941
|
issues: list[str] = []
|
|
3107
3942
|
bundle = Path(path).expanduser()
|
|
3108
3943
|
manifest_path = bundle / "contribution_manifest.json"
|
|
3109
3944
|
if not bundle.exists():
|
|
3110
3945
|
issues.append(f"Bundle path does not exist: {bundle}")
|
|
3111
3946
|
elif not manifest_path.exists():
|
|
3112
|
-
issues.append(f"
|
|
3947
|
+
issues.append(f"Experience Compilation manifest not found: {manifest_path}")
|
|
3113
3948
|
manifest_data = {}
|
|
3114
3949
|
if manifest_path.exists():
|
|
3115
3950
|
try:
|
|
@@ -3129,8 +3964,16 @@ def bundle_check(path: Path=typer.Argument(..., help='Contribution Bundle folder
|
|
|
3129
3964
|
issues.append(f"outputs/artifacts_index.json could not be parsed: {exc}")
|
|
3130
3965
|
for row in artifacts:
|
|
3131
3966
|
ref = row.get("package_relative_path") or row.get("artifact_ref")
|
|
3132
|
-
if ref
|
|
3133
|
-
|
|
3967
|
+
if ref:
|
|
3968
|
+
normalized = normalize_artifact_ref(ref)
|
|
3969
|
+
existing_refs = {
|
|
3970
|
+
str(p.relative_to(bundle)).replace("\\", "/")
|
|
3971
|
+
for p in bundle.rglob("*")
|
|
3972
|
+
if p.is_file()
|
|
3973
|
+
}
|
|
3974
|
+
candidates = artifact_ref_candidates(normalized) | {normalized}
|
|
3975
|
+
if not any((bundle / candidate).exists() for candidate in candidates) and not artifact_ref_resolves(normalized, existing_refs):
|
|
3976
|
+
issues.append(f"Referenced artifact is missing: {ref}")
|
|
3134
3977
|
for optional in ["session_events.jsonl", "progress_events.jsonl"]:
|
|
3135
3978
|
file_path = bundle / optional
|
|
3136
3979
|
if file_path.exists():
|
|
@@ -3138,39 +3981,105 @@ def bundle_check(path: Path=typer.Argument(..., help='Contribution Bundle folder
|
|
|
3138
3981
|
read_jsonl(file_path)
|
|
3139
3982
|
except Exception as exc:
|
|
3140
3983
|
issues.append(f"{optional} could not be parsed: {exc}")
|
|
3984
|
+
for ref in manifest_data.get("experience_compiler_output_refs") or []:
|
|
3985
|
+
normalized = normalize_artifact_ref(ref)
|
|
3986
|
+
if not (bundle / normalized).exists():
|
|
3987
|
+
issues.append(f"Referenced Experience Compiler output is missing: {ref}")
|
|
3988
|
+
for ref in manifest_data.get("tdo_output_refs") or []:
|
|
3989
|
+
normalized = normalize_artifact_ref(ref)
|
|
3990
|
+
if not (bundle / normalized).exists():
|
|
3991
|
+
issues.append(f"Referenced legacy learning output is missing: {ref}")
|
|
3992
|
+
for ref in manifest_data.get("loop_review_refs") or []:
|
|
3993
|
+
normalized = normalize_artifact_ref(ref)
|
|
3994
|
+
if not (bundle / normalized).exists():
|
|
3995
|
+
issues.append(f"Referenced loop review packet is missing: {ref}")
|
|
3996
|
+
tdo_dir = bundle / "tdo"
|
|
3997
|
+
if tdo_dir.exists():
|
|
3998
|
+
for file_path in tdo_dir.rglob("*.json"):
|
|
3999
|
+
try:
|
|
4000
|
+
read_json(file_path)
|
|
4001
|
+
except Exception as exc:
|
|
4002
|
+
issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
|
|
4003
|
+
for file_path in tdo_dir.rglob("*.jsonl"):
|
|
4004
|
+
try:
|
|
4005
|
+
read_jsonl(file_path)
|
|
4006
|
+
except Exception as exc:
|
|
4007
|
+
issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
|
|
4008
|
+
compiler_dir = bundle / "experience_compiler"
|
|
4009
|
+
if compiler_dir.exists():
|
|
4010
|
+
for file_path in compiler_dir.rglob("*.json"):
|
|
4011
|
+
try:
|
|
4012
|
+
read_json(file_path)
|
|
4013
|
+
except Exception as exc:
|
|
4014
|
+
issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
|
|
4015
|
+
loops_dir = bundle / "loops"
|
|
4016
|
+
if loops_dir.exists():
|
|
4017
|
+
for file_path in loops_dir.rglob("*.json"):
|
|
4018
|
+
try:
|
|
4019
|
+
read_json(file_path)
|
|
4020
|
+
except Exception as exc:
|
|
4021
|
+
issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
|
|
4022
|
+
for file_path in loops_dir.rglob("*.jsonl"):
|
|
4023
|
+
try:
|
|
4024
|
+
read_jsonl(file_path)
|
|
4025
|
+
except Exception as exc:
|
|
4026
|
+
issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
|
|
4027
|
+
for file_path in compiler_dir.rglob("*.jsonl"):
|
|
4028
|
+
try:
|
|
4029
|
+
read_jsonl(file_path)
|
|
4030
|
+
except Exception as exc:
|
|
4031
|
+
issues.append(f"{file_path.relative_to(bundle)} could not be parsed: {exc}")
|
|
4032
|
+
surface_warnings: list[str] = []
|
|
4033
|
+
for trace_path in [bundle / "traces" / "agent_traces.jsonl", bundle / "traces" / "raw_agent_traces.jsonl"]:
|
|
4034
|
+
if not trace_path.exists():
|
|
4035
|
+
continue
|
|
4036
|
+
try:
|
|
4037
|
+
for trace in read_jsonl(trace_path):
|
|
4038
|
+
for step in trace.get("steps") or []:
|
|
4039
|
+
missing = step.get("missing_surface_fields") or []
|
|
4040
|
+
status = step.get("surface_capture_status")
|
|
4041
|
+
surface = step.get("interaction_surface")
|
|
4042
|
+
if surface and status in {"partial", "unavailable"} and missing:
|
|
4043
|
+
surface_warnings.append(
|
|
4044
|
+
f"{trace_path.relative_to(bundle)} step {step.get('step')}: {surface} capture {status}; missing {', '.join(str(x) for x in missing[:6])}"
|
|
4045
|
+
)
|
|
4046
|
+
except Exception:
|
|
4047
|
+
pass
|
|
3141
4048
|
secret_hits = _bundle_secret_hits(bundle) if bundle.exists() else []
|
|
3142
4049
|
if secret_hits:
|
|
3143
4050
|
issues.append("Obvious secret-like values were found: " + ", ".join(secret_hits[:10]))
|
|
3144
4051
|
if issues:
|
|
3145
|
-
typer.echo("
|
|
3146
|
-
typer.echo(f"
|
|
4052
|
+
typer.echo("Experience Compilation check: needs attention")
|
|
4053
|
+
typer.echo(f"Experience Compilation: {bundle}")
|
|
3147
4054
|
for issue in issues:
|
|
3148
4055
|
typer.echo(f"Reason: {redact_secrets(issue)}")
|
|
3149
4056
|
typer.echo("Next action: fix the packaging issue, then rerun `apprentice bundle check`.")
|
|
3150
4057
|
raise typer.Exit(1)
|
|
3151
4058
|
status = manifest_data.get("task_status") or manifest_data.get("run_status") or "unknown"
|
|
3152
|
-
typer.echo("
|
|
3153
|
-
typer.echo(f"
|
|
4059
|
+
typer.echo("Experience Compilation check: passed")
|
|
4060
|
+
typer.echo(f"Experience Compilation: {bundle}")
|
|
3154
4061
|
typer.echo(f"Task status: {status}")
|
|
3155
|
-
|
|
4062
|
+
for warning in surface_warnings[:10]:
|
|
4063
|
+
typer.echo(f"Surface completeness warning: {redact_secrets(warning)}")
|
|
4064
|
+
typer.echo("This Experience Compilation is ready to add to the public ecosystem.")
|
|
3156
4065
|
|
|
3157
4066
|
|
|
3158
4067
|
@bundle_app.command('contribute')
|
|
3159
|
-
def bundle_contribute(path: Path=typer.Argument(..., help='
|
|
4068
|
+
def bundle_contribute(path: Path=typer.Argument(..., help='Experience Compilation folder.')):
|
|
3160
4069
|
manifest=path/'contribution_manifest.json'
|
|
3161
4070
|
if not manifest.exists():
|
|
3162
|
-
typer.echo(f'
|
|
4071
|
+
typer.echo(f'Experience Compilation manifest not found: {manifest}')
|
|
3163
4072
|
raise typer.Exit(1)
|
|
3164
4073
|
try:
|
|
3165
4074
|
submission_dir, package_zip, metadata = _create_ecosystem_submission(path)
|
|
3166
4075
|
except Exception as exc:
|
|
3167
4076
|
typer.echo(f"Could not prepare public ecosystem contribution: {redact_secrets(str(exc))}")
|
|
3168
4077
|
raise typer.Exit(1)
|
|
3169
|
-
typer.echo('
|
|
4078
|
+
typer.echo('Experience Compilation ready.')
|
|
3170
4079
|
typer.echo('')
|
|
3171
4080
|
typer.echo(f"Bundle ID: {metadata.get('bundle_id')}")
|
|
3172
4081
|
typer.echo(f"Title: {metadata.get('title')}")
|
|
3173
|
-
typer.echo('
|
|
4082
|
+
typer.echo('Experience Compilation:')
|
|
3174
4083
|
typer.echo(str(path))
|
|
3175
4084
|
typer.echo('')
|
|
3176
4085
|
typer.echo('No upload was performed by this command.')
|