agent-apprenticeship 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -197
- 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 +1229 -269
- 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 +40 -3
- 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,16 +47,36 @@ 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
|
|
79
|
+
from .public_sanitizer import sanitize_public_obj, sanitize_public_text
|
|
51
80
|
from .command_discovery import AGENT_COMMAND_CANDIDATES, resolve_command, resolve_agent_command
|
|
52
81
|
from .integration_status import integrations_report
|
|
53
82
|
from .certification import certification_exit_code, run_certification_matrix
|
|
@@ -55,6 +84,9 @@ from .learning import (
|
|
|
55
84
|
active_packs,
|
|
56
85
|
compare_replay,
|
|
57
86
|
compile_experience_pack,
|
|
87
|
+
install_runtime_training_from_bundle,
|
|
88
|
+
installed_skill_run_refs,
|
|
89
|
+
list_installed_skills,
|
|
58
90
|
list_packs,
|
|
59
91
|
load_pack,
|
|
60
92
|
pack_run_refs,
|
|
@@ -63,7 +95,10 @@ from .learning import (
|
|
|
63
95
|
resolve_learning_source,
|
|
64
96
|
resolve_packs_for_run,
|
|
65
97
|
search_learning_sources,
|
|
98
|
+
select_runtime_training_for_run,
|
|
99
|
+
set_installed_skill_enabled,
|
|
66
100
|
slugify,
|
|
101
|
+
uninstall_installed_skill,
|
|
67
102
|
update_pack_status,
|
|
68
103
|
write_before_after_result,
|
|
69
104
|
)
|
|
@@ -72,11 +107,13 @@ from .progress import append_progress_event, format_progress_event, format_run_s
|
|
|
72
107
|
from .recipes import MODEL_PROVIDER_RECIPES, WORKER_AGENT_RECIPES
|
|
73
108
|
app=typer.Typer(add_completion=False)
|
|
74
109
|
configure_app=typer.Typer(add_completion=False, help='Configure Apprentice Agents and Mentor Model Providers.')
|
|
75
|
-
ecosystem_app=typer.Typer(add_completion=False, help='Explore and
|
|
76
|
-
|
|
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.')
|
|
77
113
|
learn_app=typer.Typer(add_completion=False, help='Create and apply reversible Experience Packs from ecosystem experience.')
|
|
78
114
|
app.add_typer(configure_app, name='configure')
|
|
79
115
|
app.add_typer(ecosystem_app, name='ecosystem')
|
|
116
|
+
ecosystem_app.add_typer(ecosystem_shared_app, name='shared')
|
|
80
117
|
app.add_typer(bundle_app, name='bundle')
|
|
81
118
|
app.add_typer(learn_app, name='learn')
|
|
82
119
|
|
|
@@ -101,9 +138,9 @@ def _split_asset_prompt(text: str) -> list[Path]:
|
|
|
101
138
|
def _print_bundle_ready(bundle: Path | None) -> None:
|
|
102
139
|
if not bundle:
|
|
103
140
|
return
|
|
104
|
-
typer.echo(f'
|
|
141
|
+
typer.echo(f'Experience Compilation path: {bundle}')
|
|
105
142
|
typer.echo(f'Contribute to public ecosystem: apprentice ecosystem contribute {bundle}')
|
|
106
|
-
typer.echo(
|
|
143
|
+
typer.echo('Public ecosystem: Forsy ecosystem')
|
|
107
144
|
typer.echo(f'View files: open {bundle}')
|
|
108
145
|
|
|
109
146
|
|
|
@@ -112,7 +149,7 @@ def _print_public_ecosystem_contribution_help(bundle: str | Path) -> None:
|
|
|
112
149
|
typer.echo(f'apprentice ecosystem contribute {bundle}')
|
|
113
150
|
typer.echo('')
|
|
114
151
|
typer.echo('Public ecosystem:')
|
|
115
|
-
typer.echo(
|
|
152
|
+
typer.echo('Forsy ecosystem')
|
|
116
153
|
|
|
117
154
|
|
|
118
155
|
def _print_first_run_banner() -> None:
|
|
@@ -155,15 +192,14 @@ def _print_task_result(
|
|
|
155
192
|
typer.echo(f'{label}:')
|
|
156
193
|
typer.echo(str(artifacts))
|
|
157
194
|
if bundle:
|
|
158
|
-
typer.echo('
|
|
195
|
+
typer.echo('Experience Compilation:')
|
|
159
196
|
typer.echo(str(bundle))
|
|
160
197
|
if include_contribution_help:
|
|
161
198
|
typer.echo('')
|
|
162
|
-
mode = get_settings().
|
|
163
|
-
|
|
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":
|
|
164
202
|
_print_public_ecosystem_contribution_help(bundle)
|
|
165
|
-
else:
|
|
166
|
-
typer.echo(f'Ecosystem Auto-Share: {ecosystem_auto_share_display(mode)}')
|
|
167
203
|
typer.echo('')
|
|
168
204
|
typer.echo('View files:')
|
|
169
205
|
typer.echo(f'open {bundle}')
|
|
@@ -183,15 +219,14 @@ def _print_session_finished(status: dict, *, include_contribution_help: bool=Tru
|
|
|
183
219
|
typer.echo("Artifacts:")
|
|
184
220
|
typer.echo(str(status.get('artifacts_path')))
|
|
185
221
|
if status.get('contribution_bundle_path'):
|
|
186
|
-
typer.echo("
|
|
222
|
+
typer.echo("Experience Compilation:")
|
|
187
223
|
typer.echo(str(status.get('contribution_bundle_path')))
|
|
188
224
|
if include_contribution_help:
|
|
189
225
|
typer.echo('')
|
|
190
|
-
mode = get_settings().
|
|
191
|
-
|
|
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":
|
|
192
229
|
_print_public_ecosystem_contribution_help(status.get('contribution_bundle_path'))
|
|
193
|
-
else:
|
|
194
|
-
typer.echo(f'Ecosystem Auto-Share: {ecosystem_auto_share_display(mode)}')
|
|
195
230
|
typer.echo('')
|
|
196
231
|
typer.echo('View files:')
|
|
197
232
|
typer.echo(f"open {status.get('contribution_bundle_path')}")
|
|
@@ -326,6 +361,7 @@ def _configure_detected_model_provider(row: dict) -> None:
|
|
|
326
361
|
|
|
327
362
|
def _first_run_setup(*, interactive: bool, defaults: bool=False) -> None:
|
|
328
363
|
detected_agents = _detect_apprentice_agents()
|
|
364
|
+
detected_providers = _detect_model_provider_keys()
|
|
329
365
|
typer.echo("")
|
|
330
366
|
if detected_agents:
|
|
331
367
|
typer.echo("Detected Apprentice Agents:")
|
|
@@ -350,7 +386,6 @@ def _first_run_setup(*, interactive: bool, defaults: bool=False) -> None:
|
|
|
350
386
|
typer.echo("Or choose Custom to provide a command template.")
|
|
351
387
|
typer.echo("Then rerun: apprentice init --setup")
|
|
352
388
|
|
|
353
|
-
detected_providers = _detect_model_provider_keys()
|
|
354
389
|
typer.echo("")
|
|
355
390
|
if detected_providers:
|
|
356
391
|
typer.echo("Detected Mentor Model Provider keys:")
|
|
@@ -369,6 +404,37 @@ def _first_run_setup(*, interactive: bool, defaults: bool=False) -> None:
|
|
|
369
404
|
typer.echo("No Mentor Model Provider API key was detected.")
|
|
370
405
|
_print_key_storage_guidance()
|
|
371
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
|
+
|
|
372
438
|
|
|
373
439
|
def _progress_callback(quiet: bool=False, verbose: bool=False, json_progress: bool=False):
|
|
374
440
|
if quiet:
|
|
@@ -383,7 +449,7 @@ def _progress_callback(quiet: bool=False, verbose: bool=False, json_progress: bo
|
|
|
383
449
|
typer.echo('')
|
|
384
450
|
typer.echo(f'Run: {status.get("run_id")}')
|
|
385
451
|
typer.echo(f'Apprentice Agent: {status.get("apprentice_agent") or status.get("worker_agent")}')
|
|
386
|
-
typer.echo(f'
|
|
452
|
+
typer.echo(f'Apprenticeship Mode: {apprenticeship_mode_display(status.get("apprenticeship_mode") or status.get("mentor_mode"))}')
|
|
387
453
|
typer.echo(f'Maximum Improvement Loops: {status.get("maximum_improvement_loops")}')
|
|
388
454
|
typer.echo(f'Task workspace: {status.get("task_workspace_path")}')
|
|
389
455
|
typer.echo(f'Artifacts: {status.get("artifacts_path")}')
|
|
@@ -398,6 +464,16 @@ def _progress_callback(quiet: bool=False, verbose: bool=False, json_progress: bo
|
|
|
398
464
|
if event_type in {'followup_completed'}:
|
|
399
465
|
_print_task_result(status, followup=True, record_only=bool((event.get('metadata_json') or {}).get('record_only')))
|
|
400
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
|
|
401
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:
|
|
402
478
|
typer.echo(format_progress_event(event))
|
|
403
479
|
return _emit
|
|
@@ -459,7 +535,7 @@ def _normalize_evaluation_mode(value: str | None) -> str | None:
|
|
|
459
535
|
return None
|
|
460
536
|
normalized=value.strip().lower().replace('_','-')
|
|
461
537
|
if normalized not in EVALUATION_MODES:
|
|
462
|
-
raise typer.BadParameter(f'
|
|
538
|
+
raise typer.BadParameter(f'apprenticeship mode must be one of: {", ".join(APPRENTICESHIP_MODES)}')
|
|
463
539
|
return normalized
|
|
464
540
|
|
|
465
541
|
def _normalize_mentor_mode(value: str | None) -> str | None:
|
|
@@ -468,7 +544,7 @@ def _normalize_mentor_mode(value: str | None) -> str | None:
|
|
|
468
544
|
try:
|
|
469
545
|
return normalize_mentor_mode(value)
|
|
470
546
|
except ValueError as exc:
|
|
471
|
-
raise typer.BadParameter(f'
|
|
547
|
+
raise typer.BadParameter(f'apprenticeship mode must be one of: {", ".join(APPRENTICESHIP_MODES)}') from exc
|
|
472
548
|
|
|
473
549
|
def _normalize_data_sharing(value: str | None) -> str | None:
|
|
474
550
|
if value is None:
|
|
@@ -500,10 +576,33 @@ def _normalize_auto_share(value: str | None) -> str | None:
|
|
|
500
576
|
return normalize_ecosystem_auto_share(value)
|
|
501
577
|
except ValueError as exc:
|
|
502
578
|
raise typer.BadParameter(
|
|
503
|
-
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)}'
|
|
504
602
|
) from exc
|
|
505
603
|
|
|
506
604
|
def _settings_for_run(
|
|
605
|
+
apprenticeship_mode: str | None=None,
|
|
507
606
|
mentor_mode: str | None=None,
|
|
508
607
|
sensitive_info_masking: str | None=None,
|
|
509
608
|
max_loops: int | None=None,
|
|
@@ -512,12 +611,15 @@ def _settings_for_run(
|
|
|
512
611
|
):
|
|
513
612
|
settings=get_settings()
|
|
514
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)
|
|
515
617
|
if mentor_mode is not None:
|
|
516
|
-
updates['
|
|
618
|
+
updates['apprenticeship_mode']=_normalize_apprenticeship_mode(mentor_mode)
|
|
517
619
|
if evaluation_mode is not None:
|
|
518
620
|
updates['evaluation_mode']=_normalize_evaluation_mode(evaluation_mode)
|
|
519
|
-
if mentor_mode is None:
|
|
520
|
-
updates['
|
|
621
|
+
if mentor_mode is None and apprenticeship_mode is None:
|
|
622
|
+
updates['apprenticeship_mode']=_normalize_apprenticeship_mode(evaluation_mode)
|
|
521
623
|
if sensitive_info_masking is not None:
|
|
522
624
|
updates['sensitive_info_masking']=_normalize_masking(sensitive_info_masking)
|
|
523
625
|
if data_sharing_level is not None:
|
|
@@ -529,6 +631,12 @@ def _settings_for_run(
|
|
|
529
631
|
raise typer.BadParameter('maximum improvement loops must be at least 1')
|
|
530
632
|
updates['max_improvement_loops']=max_loops
|
|
531
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"
|
|
532
640
|
return settings.model_copy(update=updates)
|
|
533
641
|
|
|
534
642
|
def _configure_model_impl(provider: str | None, model: str | None, api_key_env_var: str | None, test_connection: bool):
|
|
@@ -608,15 +716,15 @@ def _ensure_evaluation_ready(settings, interactive: bool=True):
|
|
|
608
716
|
if settings.mentor_mode == 'expert_led' or model_provider_ready(settings):
|
|
609
717
|
return settings
|
|
610
718
|
if not interactive:
|
|
611
|
-
raise typer.BadParameter('
|
|
719
|
+
raise typer.BadParameter('Autonomous Apprenticeship requires a ready Mentor Model Provider; use configure model or --apprenticeship-mode expert-led')
|
|
612
720
|
if not _stdin_allows_prompt():
|
|
613
721
|
readiness = mentor_model_provider_readiness(settings)
|
|
614
722
|
reason = readiness.get("reason") or "Mentor Model Provider is not ready."
|
|
615
723
|
raise typer.BadParameter(
|
|
616
|
-
f"
|
|
617
|
-
"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`."
|
|
618
726
|
)
|
|
619
|
-
typer.echo('
|
|
727
|
+
typer.echo('Autonomous Apprenticeship needs a ready Mentor Model Provider.')
|
|
620
728
|
readiness=mentor_model_provider_readiness(settings)
|
|
621
729
|
if readiness.get("provider"):
|
|
622
730
|
typer.echo(_status_line('Mentor Model Provider Status', readiness.get("status"), readiness.get("reason")))
|
|
@@ -631,14 +739,14 @@ def _ensure_evaluation_ready(settings, interactive: bool=True):
|
|
|
631
739
|
except typer.Abort:
|
|
632
740
|
reason = readiness.get("reason") or "Mentor Model Provider is not ready."
|
|
633
741
|
raise typer.BadParameter(
|
|
634
|
-
f"
|
|
635
|
-
"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`."
|
|
636
744
|
) from None
|
|
637
745
|
if choice in {'1','setup','set up','configure','configure mentor model provider','set up model provider now'}:
|
|
638
746
|
configured=_configure_model_impl(None, None, None, test_connection=False)
|
|
639
747
|
return configured
|
|
640
748
|
if choice in {'2','expert-led','expert','manual','run this session in expert-led mode'}:
|
|
641
|
-
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'})
|
|
642
750
|
raise typer.Exit(1)
|
|
643
751
|
|
|
644
752
|
|
|
@@ -726,6 +834,7 @@ def init(
|
|
|
726
834
|
):
|
|
727
835
|
_print_first_run_banner()
|
|
728
836
|
path=init_settings(overwrite=overwrite)
|
|
837
|
+
get_or_create_ecosystem_identity(get_settings())
|
|
729
838
|
typer.echo(f'initialized Agent Apprenticeship settings at {path}')
|
|
730
839
|
if defaults:
|
|
731
840
|
_first_run_setup(interactive=False, defaults=True)
|
|
@@ -773,7 +882,7 @@ def doctor(
|
|
|
773
882
|
typer.echo(f'Apprentice Agent command: {agent.get("command") or "Not configured"}')
|
|
774
883
|
typer.echo(f'Apprentice Agent command found: {_yes_no(bool(agent.get("command_found")))}')
|
|
775
884
|
typer.echo(_status_line('Apprentice Agent readiness', agent.get('status'), agent.get('reason')))
|
|
776
|
-
typer.echo(f'
|
|
885
|
+
typer.echo(f'Apprenticeship Mode: {apprenticeship_mode_display(settings.apprenticeship_mode)}')
|
|
777
886
|
typer.echo(f'Mentor Model Provider: {provider.get("provider_display")}')
|
|
778
887
|
typer.echo(f'Mentor model: {provider.get("model") or "Not configured"}')
|
|
779
888
|
typer.echo(f'API key env var: {provider.get("api_key_env_var") or "Not configured"}')
|
|
@@ -1208,13 +1317,16 @@ def configure_agent(
|
|
|
1208
1317
|
|
|
1209
1318
|
@configure_app.command('settings')
|
|
1210
1319
|
def configure_settings(
|
|
1211
|
-
|
|
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),
|
|
1212
1322
|
sensitive_info_masking: str | None=typer.Option(None, '--sensitive-info-masking', help='standard or no-masking.'),
|
|
1213
1323
|
max_loops: int | None=typer.Option(None, '--max-loops', help='Maximum improvement loops.'),
|
|
1214
1324
|
):
|
|
1215
1325
|
updates={}
|
|
1326
|
+
if apprenticeship_mode is not None:
|
|
1327
|
+
updates['apprenticeship_mode']=_normalize_apprenticeship_mode(apprenticeship_mode)
|
|
1216
1328
|
if mentor_mode is not None:
|
|
1217
|
-
updates['
|
|
1329
|
+
updates['apprenticeship_mode']=_normalize_apprenticeship_mode(mentor_mode)
|
|
1218
1330
|
if sensitive_info_masking is not None:
|
|
1219
1331
|
updates['sensitive_info_masking']=_normalize_masking(sensitive_info_masking)
|
|
1220
1332
|
if max_loops is not None:
|
|
@@ -1227,6 +1339,46 @@ def configure_settings(
|
|
|
1227
1339
|
update_settings(**updates)
|
|
1228
1340
|
typer.echo(public_settings_text())
|
|
1229
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
|
+
|
|
1230
1382
|
@configure_app.command('model')
|
|
1231
1383
|
def configure_model(
|
|
1232
1384
|
provider: str | None=typer.Argument(None, help='Known Mentor Model Provider id.'),
|
|
@@ -1240,18 +1392,19 @@ def configure_model(
|
|
|
1240
1392
|
def start(
|
|
1241
1393
|
asset: list[Path]=typer.Option([], '--asset', '-a', help='Optional task asset file or directory.'),
|
|
1242
1394
|
runner: str | None=typer.Option(None, '--runner', help='Advanced compatibility runner override.'),
|
|
1243
|
-
|
|
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),
|
|
1244
1397
|
sensitive_info_masking: str | None=typer.Option(None, '--sensitive-info-masking', help='standard or no-masking.'),
|
|
1245
|
-
evaluation_mode: str | None=typer.Option(None, '--evaluation-mode', help='Backward-compatible alias for --
|
|
1246
|
-
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),
|
|
1247
1400
|
max_loops: int | None=typer.Option(None, '--max-loops', help='Maximum improvement loops for this run.'),
|
|
1248
1401
|
quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
|
|
1249
1402
|
verbose: bool=typer.Option(False, '--verbose', help='Print additional progress details when available.'),
|
|
1250
1403
|
json_progress: bool=typer.Option(False, '--json-progress', help='Emit progress events as JSONL.'),
|
|
1251
1404
|
expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
|
|
1252
|
-
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),
|
|
1253
1406
|
expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led checkpoint inputs even in non-TTY test harnesses.'),
|
|
1254
|
-
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),
|
|
1255
1408
|
):
|
|
1256
1409
|
current_settings = get_settings()
|
|
1257
1410
|
current_agent = apprentice_agent_readiness_status(current_settings)
|
|
@@ -1260,7 +1413,7 @@ def start(
|
|
|
1260
1413
|
instruction=typer.prompt('What should your agent work on?')
|
|
1261
1414
|
extra=typer.prompt('Add assets? optional', default='', show_default=False)
|
|
1262
1415
|
assets=list(asset)+_split_asset_prompt(extra)
|
|
1263
|
-
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)
|
|
1264
1417
|
_ensure_apprentice_agent_ready(settings, runner, interactive=True)
|
|
1265
1418
|
interactive_checkpoints = _interactive_checkpoint_requested(
|
|
1266
1419
|
settings,
|
|
@@ -1294,10 +1447,11 @@ def run_prompt(
|
|
|
1294
1447
|
asset: list[Path]=typer.Option([], '--asset', '-a', help='Optional task asset file or directory.'),
|
|
1295
1448
|
run_id: str | None=typer.Option(None, '--run-id', help='Optional run id/slug.'),
|
|
1296
1449
|
runner: str | None=typer.Option(None, '--runner', help='Advanced compatibility runner override.'),
|
|
1297
|
-
|
|
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),
|
|
1298
1452
|
sensitive_info_masking: str | None=typer.Option(None, '--sensitive-info-masking', help='standard or no-masking.'),
|
|
1299
|
-
evaluation_mode: str | None=typer.Option(None, '--evaluation-mode', help='Backward-compatible alias for --
|
|
1300
|
-
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),
|
|
1301
1455
|
max_loops: int | None=typer.Option(None, '--max-loops', help='Maximum improvement loops for this run.'),
|
|
1302
1456
|
quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
|
|
1303
1457
|
verbose: bool=typer.Option(False, '--verbose', help='Print additional progress details when available.'),
|
|
@@ -1305,12 +1459,14 @@ def run_prompt(
|
|
|
1305
1459
|
experience_pack: list[str]=typer.Option([], '--experience-pack', help='Experience Pack id/path to apply to this run.'),
|
|
1306
1460
|
no_experience_packs: bool=typer.Option(False, '--no-experience-packs', help='Do not apply Experience Packs to this run.'),
|
|
1307
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.'),
|
|
1308
1464
|
expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
|
|
1309
|
-
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),
|
|
1310
1466
|
expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led checkpoint inputs even in non-TTY test harnesses.'),
|
|
1311
|
-
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),
|
|
1312
1468
|
):
|
|
1313
|
-
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)
|
|
1314
1470
|
_ensure_apprentice_agent_ready(settings, runner, interactive=True)
|
|
1315
1471
|
for pack_id in experience_pack:
|
|
1316
1472
|
try:
|
|
@@ -1326,8 +1482,26 @@ def run_prompt(
|
|
|
1326
1482
|
no_packs=no_experience_packs,
|
|
1327
1483
|
settings=settings,
|
|
1328
1484
|
)
|
|
1329
|
-
|
|
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
|
|
1330
1503
|
experience_refs = pack_run_refs(selected_packs)
|
|
1504
|
+
runtime_refs = installed_skill_run_refs(installed_skills)
|
|
1331
1505
|
interactive_checkpoints = _interactive_checkpoint_requested(
|
|
1332
1506
|
settings,
|
|
1333
1507
|
expert_auto_approve=expert_auto_approve,
|
|
@@ -1349,6 +1523,7 @@ def run_prompt(
|
|
|
1349
1523
|
runner=runner,
|
|
1350
1524
|
progress_callback=_progress_callback(quiet, verbose, json_progress),
|
|
1351
1525
|
experience_pack_refs=experience_refs,
|
|
1526
|
+
runtime_training_refs=runtime_refs,
|
|
1352
1527
|
)
|
|
1353
1528
|
except RunInterrupted as exc:
|
|
1354
1529
|
if not quiet and not json_progress:
|
|
@@ -1368,15 +1543,15 @@ def continue_run(
|
|
|
1368
1543
|
followup_instruction: str | None=typer.Argument(None, help='Optional direct follow-up instruction.'),
|
|
1369
1544
|
asset: list[Path]=typer.Option([], '--asset', '-a', help='Optional follow-up asset file or directory.'),
|
|
1370
1545
|
run_loop: bool=typer.Option(False, '--run-loop', help='Run another improvement loop after recording the follow-up.'),
|
|
1371
|
-
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.'),
|
|
1372
1547
|
runner: str | None=typer.Option(None, '--runner', help='Advanced compatibility runner override.'),
|
|
1373
1548
|
quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
|
|
1374
1549
|
verbose: bool=typer.Option(False, '--verbose', help='Print additional progress details when available.'),
|
|
1375
1550
|
json_progress: bool=typer.Option(False, '--json-progress', help='Emit progress events as JSONL.'),
|
|
1376
1551
|
expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led checkpoint inputs even in non-TTY test harnesses.'),
|
|
1377
|
-
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),
|
|
1378
1553
|
expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
|
|
1379
|
-
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),
|
|
1380
1555
|
):
|
|
1381
1556
|
settings=get_settings()
|
|
1382
1557
|
run_root=run_root_for(run_id, settings)
|
|
@@ -1428,7 +1603,7 @@ def continue_run(
|
|
|
1428
1603
|
typer.echo("")
|
|
1429
1604
|
typer.echo("Follow-up interrupted.")
|
|
1430
1605
|
if previous_status.get("contribution_bundle_path"):
|
|
1431
|
-
typer.echo("Previous
|
|
1606
|
+
typer.echo("Previous Experience Compilation:")
|
|
1432
1607
|
typer.echo(str(previous_status.get("contribution_bundle_path")))
|
|
1433
1608
|
raise typer.Exit(130)
|
|
1434
1609
|
status = read_run_status(run_root)
|
|
@@ -1442,9 +1617,9 @@ def finish(
|
|
|
1442
1617
|
quiet: bool=typer.Option(False, '--quiet', help='Print only the final task result.'),
|
|
1443
1618
|
json_progress: bool=typer.Option(False, '--json-progress', help='Emit progress events as JSONL.'),
|
|
1444
1619
|
expert_interactive: bool=typer.Option(False, '--expert-interactive', help='Prompt for expert-led final checkpoint inputs even in non-TTY test harnesses.'),
|
|
1445
|
-
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),
|
|
1446
1621
|
expert_auto_approve: bool=typer.Option(False, '--expert-auto-approve', help='Test/dev helper: auto-approve expert-led checkpoints.'),
|
|
1447
|
-
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),
|
|
1448
1623
|
):
|
|
1449
1624
|
settings = get_settings()
|
|
1450
1625
|
interactive_checkpoints = _interactive_checkpoint_requested(
|
|
@@ -1459,7 +1634,7 @@ def finish(
|
|
|
1459
1634
|
hybrid_auto_approve=hybrid_auto_approve,
|
|
1460
1635
|
mentor_interactive_checkpoints=interactive_checkpoints,
|
|
1461
1636
|
):
|
|
1462
|
-
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))
|
|
1463
1638
|
status=read_run_status(run_root)
|
|
1464
1639
|
if json_progress:
|
|
1465
1640
|
for event in read_jsonl(run_root/'progress_events.jsonl'):
|
|
@@ -1532,6 +1707,8 @@ def _ecosystem_status() -> dict:
|
|
|
1532
1707
|
repo_path = _configured_ecosystem_repo_path()
|
|
1533
1708
|
gh_installed = bool(_gh_path())
|
|
1534
1709
|
gh_authed = _gh_authenticated() if gh_installed else False
|
|
1710
|
+
settings = get_settings()
|
|
1711
|
+
r2 = r2_status(settings)
|
|
1535
1712
|
return {
|
|
1536
1713
|
"repo": repo,
|
|
1537
1714
|
"repo_url": _ecosystem_repo_url(repo),
|
|
@@ -1540,10 +1717,22 @@ def _ecosystem_status() -> dict:
|
|
|
1540
1717
|
"gh_installed": gh_installed,
|
|
1541
1718
|
"gh_authenticated": gh_authed,
|
|
1542
1719
|
"automatic_contribution_ready": bool(repo and gh_installed and gh_authed),
|
|
1720
|
+
**r2,
|
|
1543
1721
|
}
|
|
1544
1722
|
|
|
1545
1723
|
|
|
1546
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
|
+
}
|
|
1547
1736
|
configured_repo = repo or _configured_ecosystem_repo() or DEFAULT_PUBLIC_ECOSYSTEM_REPO
|
|
1548
1737
|
gh_installed = bool(_gh_path())
|
|
1549
1738
|
gh_authed = _gh_authenticated() if gh_installed else False
|
|
@@ -1591,13 +1780,15 @@ def _ecosystem_auto_share_readiness(repo: str | None = None) -> dict:
|
|
|
1591
1780
|
def _ecosystem_auto_share_status_line(settings=None) -> str:
|
|
1592
1781
|
s = settings or get_settings()
|
|
1593
1782
|
readiness = _ecosystem_auto_share_readiness()
|
|
1594
|
-
label =
|
|
1595
|
-
if s.
|
|
1596
|
-
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"
|
|
1597
1788
|
if readiness["ready"]:
|
|
1598
|
-
return f"
|
|
1789
|
+
return f"Agent Training Contribution Mode: {label}\nPublic Ecosystem sharing behavior: {behavior_label}\nPublic Ecosystem upload status: Ready"
|
|
1599
1790
|
reason = readiness.get("reason") or "not ready"
|
|
1600
|
-
return f"
|
|
1791
|
+
return f"Agent Training Contribution Mode: {label}\nPublic Ecosystem sharing behavior: {behavior_label}\nPublic Ecosystem upload status: Not ready - {reason}"
|
|
1601
1792
|
|
|
1602
1793
|
|
|
1603
1794
|
def _public_settings_text_with_ecosystem() -> str:
|
|
@@ -1617,7 +1808,7 @@ def _public_settings_text_with_ecosystem() -> str:
|
|
|
1617
1808
|
out.append(f"GitHub URL: {_ecosystem_repo_url(settings.ecosystem_repo)}")
|
|
1618
1809
|
out.append(git_line)
|
|
1619
1810
|
inserted = True
|
|
1620
|
-
if line.startswith("
|
|
1811
|
+
if line.startswith("Agent Training Contribution Mode:"):
|
|
1621
1812
|
out.pop()
|
|
1622
1813
|
out.extend(auto_status)
|
|
1623
1814
|
if not inserted:
|
|
@@ -1900,8 +2091,16 @@ def _load_registry_file(path: Path) -> list[dict]:
|
|
|
1900
2091
|
return [row for row in rows if isinstance(row, dict)]
|
|
1901
2092
|
|
|
1902
2093
|
|
|
1903
|
-
def _load_registry(registry: Path | None=None) -> list[dict]:
|
|
2094
|
+
def _load_registry(registry: Path | None=None, storage: str | None = None) -> list[dict]:
|
|
1904
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
|
|
1905
2104
|
repo_path = _configured_ecosystem_repo_path()
|
|
1906
2105
|
if repo_path:
|
|
1907
2106
|
return _index_from_public_repo_path(repo_path)
|
|
@@ -1942,20 +2141,67 @@ def _load_registry(registry: Path | None=None) -> list[dict]:
|
|
|
1942
2141
|
return _load_registry_file(path)
|
|
1943
2142
|
|
|
1944
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
|
+
|
|
1945
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
|
|
1946
2172
|
if query:
|
|
1947
|
-
haystack=' '.join(
|
|
1948
|
-
|
|
1949
|
-
|
|
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
|
+
))
|
|
1950
2180
|
if query.lower() not in haystack.lower():
|
|
1951
2181
|
return False
|
|
1952
|
-
|
|
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']:
|
|
1953
2192
|
value=filters.get(key)
|
|
1954
2193
|
if value and str(row.get(key) or '').lower() != str(value).lower():
|
|
1955
2194
|
return False
|
|
1956
|
-
|
|
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',))]:
|
|
1957
2203
|
value=filters.get(key)
|
|
1958
|
-
if value and
|
|
2204
|
+
if value and not _row_contains(row, str(value), *fields):
|
|
1959
2205
|
return False
|
|
1960
2206
|
if filters.get('min_traced_steps') is not None and int(row.get('traced_steps') or 0) < filters['min_traced_steps']:
|
|
1961
2207
|
return False
|
|
@@ -1986,12 +2232,51 @@ def _matches_registry_row(row: dict, query: str | None, filters: dict) -> bool:
|
|
|
1986
2232
|
return True
|
|
1987
2233
|
|
|
1988
2234
|
|
|
1989
|
-
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
|
|
1990
2273
|
if not rows:
|
|
1991
|
-
typer.echo('No
|
|
2274
|
+
typer.echo('No Experience Compilations matched.')
|
|
1992
2275
|
return
|
|
1993
|
-
for row in rows:
|
|
1994
|
-
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)
|
|
1995
2280
|
|
|
1996
2281
|
|
|
1997
2282
|
def _is_seed_registry_row(row: dict) -> bool:
|
|
@@ -2002,7 +2287,7 @@ def _format_count(value) -> str:
|
|
|
2002
2287
|
return str(int(value)) if isinstance(value, (int, float)) or str(value).isdigit() else "0"
|
|
2003
2288
|
|
|
2004
2289
|
|
|
2005
|
-
def _format_seed_registry_row(row: dict) -> str:
|
|
2290
|
+
def _format_seed_registry_row(row: dict, *, index: int | None = None) -> str:
|
|
2006
2291
|
bundle_id = row.get("bundle_id") or row.get("seed_task_id") or row.get("task_id")
|
|
2007
2292
|
status = row.get("task_status") or "ready_to_run"
|
|
2008
2293
|
run_status = row.get("run_status") or "not_run"
|
|
@@ -2024,34 +2309,46 @@ def _format_seed_registry_row(row: dict) -> str:
|
|
|
2024
2309
|
if row.get("revision_preference_pairs") is not None:
|
|
2025
2310
|
details.append(f"preferences: {_format_count(row.get('revision_preference_pairs'))}")
|
|
2026
2311
|
details.append(learning_label)
|
|
2027
|
-
|
|
2312
|
+
prefix = f"{index}. " if index is not None else ""
|
|
2313
|
+
return f"{prefix}{bundle_id}: {row.get('title')} [{' · '.join(details)}]"
|
|
2028
2314
|
|
|
2029
2315
|
|
|
2030
|
-
def _format_registry_row(row: dict) -> str:
|
|
2316
|
+
def _format_registry_row(row: dict, *, index: int | None = None) -> str:
|
|
2031
2317
|
if _is_seed_registry_row(row):
|
|
2032
|
-
return _format_seed_registry_row(row)
|
|
2318
|
+
return _format_seed_registry_row(row, index=index)
|
|
2033
2319
|
details = [
|
|
2034
2320
|
str(row.get("task_status") or row.get("run_status") or "status_unknown"),
|
|
2035
2321
|
]
|
|
2036
|
-
|
|
2037
|
-
|
|
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)))}")
|
|
2038
2325
|
traces = row.get("traced_steps")
|
|
2039
2326
|
if traces is not None:
|
|
2040
2327
|
details.append(f"traces: {_format_count(traces)}")
|
|
2041
2328
|
artifacts = row.get("artifact_count")
|
|
2042
2329
|
if artifacts is not None:
|
|
2043
2330
|
details.append(f"artifacts: {_format_count(artifacts)}")
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
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
|
+
])
|
|
2048
2345
|
|
|
2049
2346
|
|
|
2050
2347
|
def _safe_bundle_path(path: Path) -> Path:
|
|
2051
2348
|
bundle = path.expanduser()
|
|
2052
2349
|
manifest = bundle / "contribution_manifest.json"
|
|
2053
2350
|
if not manifest.exists():
|
|
2054
|
-
raise FileNotFoundError(f"
|
|
2351
|
+
raise FileNotFoundError(f"Experience Compilation manifest not found: {manifest}")
|
|
2055
2352
|
return bundle
|
|
2056
2353
|
|
|
2057
2354
|
|
|
@@ -2102,6 +2399,7 @@ def _bundle_submission_metadata(bundle: Path, package_name: str | None = None, p
|
|
|
2102
2399
|
"task_status": manifest.get("task_status"),
|
|
2103
2400
|
"run_status": manifest.get("run_status"),
|
|
2104
2401
|
"apprentice_agent": session.get("apprentice_agent"),
|
|
2402
|
+
"apprenticeship_mode": manifest.get("apprenticeship_mode") or session.get("apprenticeship_mode"),
|
|
2105
2403
|
"mentor_mode": manifest.get("mentor_mode"),
|
|
2106
2404
|
"mentor_model_provider": session.get("model_provider"),
|
|
2107
2405
|
"domains": manifest.get("domains") or [],
|
|
@@ -2149,7 +2447,7 @@ def _submission_summary(metadata: dict, bundle: Path, package_zip: Path | None =
|
|
|
2149
2447
|
f"Task Status: {metadata.get('task_status')}",
|
|
2150
2448
|
f"Run Status: {metadata.get('run_status')}",
|
|
2151
2449
|
f"Apprentice Agent: {metadata.get('apprentice_agent') or 'unknown'}",
|
|
2152
|
-
f"
|
|
2450
|
+
f"Apprenticeship Mode: {apprenticeship_mode_display(metadata.get('apprenticeship_mode') or metadata.get('mentor_mode'))}",
|
|
2153
2451
|
f"Mentor Model Provider: {metadata.get('mentor_model_provider') or 'none'}",
|
|
2154
2452
|
f"Attempts: {metadata.get('attempts')}",
|
|
2155
2453
|
f"Traced Steps: {metadata.get('traced_steps')}",
|
|
@@ -2188,12 +2486,61 @@ def _zip_dir(src: Path, dst: Path) -> str:
|
|
|
2188
2486
|
return f"sha256:{digest}"
|
|
2189
2487
|
|
|
2190
2488
|
|
|
2191
|
-
def
|
|
2489
|
+
def _sanitize_submission_copy(root: Path) -> None:
|
|
2490
|
+
text_suffixes = {
|
|
2491
|
+
".json",
|
|
2492
|
+
".jsonl",
|
|
2493
|
+
".md",
|
|
2494
|
+
".txt",
|
|
2495
|
+
".yaml",
|
|
2496
|
+
".yml",
|
|
2497
|
+
".csv",
|
|
2498
|
+
".tsv",
|
|
2499
|
+
".log",
|
|
2500
|
+
}
|
|
2501
|
+
for path in root.rglob("*"):
|
|
2502
|
+
if not path.is_file() or path.stat().st_size > 5_000_000:
|
|
2503
|
+
continue
|
|
2504
|
+
if path.suffix.lower() not in text_suffixes:
|
|
2505
|
+
continue
|
|
2506
|
+
raw = path.read_text(errors="ignore")
|
|
2507
|
+
if path.suffix.lower() == ".json":
|
|
2508
|
+
try:
|
|
2509
|
+
data = json.loads(raw or "{}")
|
|
2510
|
+
except Exception:
|
|
2511
|
+
path.write_text(sanitize_public_text(raw) or "")
|
|
2512
|
+
else:
|
|
2513
|
+
if isinstance(data, dict):
|
|
2514
|
+
write_json(path, sanitize_public_obj(data))
|
|
2515
|
+
elif isinstance(data, list):
|
|
2516
|
+
path.write_text(json.dumps(sanitize_public_obj({"items": data})["items"], indent=2, sort_keys=True) + "\n")
|
|
2517
|
+
else:
|
|
2518
|
+
path.write_text(sanitize_public_text(raw) or "")
|
|
2519
|
+
elif path.suffix.lower() == ".jsonl":
|
|
2520
|
+
rows = []
|
|
2521
|
+
for line in raw.splitlines():
|
|
2522
|
+
if not line.strip():
|
|
2523
|
+
continue
|
|
2524
|
+
try:
|
|
2525
|
+
data = json.loads(line)
|
|
2526
|
+
except Exception:
|
|
2527
|
+
rows.append(json.dumps(sanitize_public_text(line) or "", sort_keys=True))
|
|
2528
|
+
else:
|
|
2529
|
+
if isinstance(data, dict):
|
|
2530
|
+
rows.append(json.dumps(sanitize_public_obj(data), sort_keys=True))
|
|
2531
|
+
else:
|
|
2532
|
+
rows.append(json.dumps(sanitize_public_obj({"item": data})["item"], sort_keys=True))
|
|
2533
|
+
path.write_text("\n".join(rows) + ("\n" if rows else ""))
|
|
2534
|
+
else:
|
|
2535
|
+
path.write_text(sanitize_public_text(raw) or "")
|
|
2536
|
+
|
|
2537
|
+
|
|
2538
|
+
def _create_ecosystem_submission(bundle: Path, *, block_secret_like_values: bool = False) -> tuple[Path, Path, dict]:
|
|
2192
2539
|
bundle = _safe_bundle_path(bundle)
|
|
2193
2540
|
secret_hits = _bundle_secret_hits(bundle)
|
|
2194
|
-
if secret_hits:
|
|
2541
|
+
if block_secret_like_values and secret_hits:
|
|
2195
2542
|
raise RuntimeError(
|
|
2196
|
-
"Obvious secret-like values were found in the
|
|
2543
|
+
"Obvious secret-like values were found in the Experience Compilation. "
|
|
2197
2544
|
"Remove or regenerate the affected files before public contribution: "
|
|
2198
2545
|
+ ", ".join(secret_hits[:10])
|
|
2199
2546
|
)
|
|
@@ -2206,6 +2553,7 @@ def _create_ecosystem_submission(bundle: Path) -> tuple[Path, Path, dict]:
|
|
|
2206
2553
|
submission_dir / "contribution_bundle",
|
|
2207
2554
|
ignore=shutil.ignore_patterns(".env", ".env.local", "__pycache__", "*.pyc"),
|
|
2208
2555
|
)
|
|
2556
|
+
_sanitize_submission_copy(submission_dir / "contribution_bundle")
|
|
2209
2557
|
package_zip = bundle.parent / f"{bundle.name}_ecosystem_submission.zip"
|
|
2210
2558
|
metadata = _bundle_submission_metadata(bundle, package_name=package_zip.name)
|
|
2211
2559
|
write_json(submission_dir / "ecosystem_submission.json", metadata)
|
|
@@ -2229,6 +2577,7 @@ def _ecosystem_contribution_record(
|
|
|
2229
2577
|
return {
|
|
2230
2578
|
"bundle_id": metadata.get("bundle_id"),
|
|
2231
2579
|
"auto_share_mode": auto_share_mode,
|
|
2580
|
+
"training_contribution_mode": auto_share_mode,
|
|
2232
2581
|
"contribution_created": contribution_created,
|
|
2233
2582
|
"contribution_url": contribution_url,
|
|
2234
2583
|
"contribution_method": contribution_method,
|
|
@@ -2250,10 +2599,15 @@ def _ecosystem_contribute_impl(
|
|
|
2250
2599
|
repo: str | None = None,
|
|
2251
2600
|
dry_run: bool = False,
|
|
2252
2601
|
auto_share_mode: str = "manual",
|
|
2602
|
+
storage: str | None = None,
|
|
2603
|
+
qa: bool = False,
|
|
2253
2604
|
) -> dict:
|
|
2254
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"}
|
|
2255
2607
|
submission_dir, package_zip, metadata = _create_ecosystem_submission(bundle)
|
|
2256
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)
|
|
2257
2611
|
issue_body = _gh_issue_body(metadata, submission_dir, package_zip)
|
|
2258
2612
|
result = {
|
|
2259
2613
|
"bundle": bundle,
|
|
@@ -2262,14 +2616,84 @@ def _ecosystem_contribute_impl(
|
|
|
2262
2616
|
"metadata": metadata,
|
|
2263
2617
|
"issue_body": issue_body,
|
|
2264
2618
|
"target_repo": target_repo,
|
|
2619
|
+
"storage_backend": storage_backend,
|
|
2620
|
+
"storage_ref": None,
|
|
2621
|
+
"ecosystem_item": None,
|
|
2622
|
+
"upload_response": None,
|
|
2265
2623
|
"contribution_created": False,
|
|
2266
2624
|
"contribution_url": None,
|
|
2267
2625
|
"contribution_method": "manual_package",
|
|
2268
2626
|
"skipped_reason": None,
|
|
2269
2627
|
"gh_error": None,
|
|
2270
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
|
+
|
|
2271
2695
|
if target_repo and _gh_path() and _gh_authenticated() and not dry_run:
|
|
2272
|
-
title = f"
|
|
2696
|
+
title = f"Experience Compilation: {metadata.get('bundle_id')}"
|
|
2273
2697
|
cp = _run_gh(["issue", "create", "--repo", target_repo, "--title", title, "--body-file", str(issue_body)], timeout=60)
|
|
2274
2698
|
if cp.returncode == 0:
|
|
2275
2699
|
result["contribution_created"] = True
|
|
@@ -2305,12 +2729,12 @@ def _bundle_from_status(status: dict) -> Path | None:
|
|
|
2305
2729
|
|
|
2306
2730
|
|
|
2307
2731
|
def _print_auto_share_summary(metadata: dict) -> None:
|
|
2308
|
-
typer.echo("
|
|
2732
|
+
typer.echo("Experience Compilation summary")
|
|
2309
2733
|
typer.echo(f"bundle_id: {metadata.get('bundle_id')}")
|
|
2310
2734
|
typer.echo(f"title: {metadata.get('title')}")
|
|
2311
2735
|
typer.echo(f"task_status: {metadata.get('task_status')}")
|
|
2312
2736
|
typer.echo(f"Apprentice Agent: {metadata.get('apprentice_agent') or 'unknown'}")
|
|
2313
|
-
typer.echo(f"
|
|
2737
|
+
typer.echo(f"Apprenticeship Mode: {apprenticeship_mode_display(metadata.get('apprenticeship_mode') or metadata.get('mentor_mode'))}")
|
|
2314
2738
|
typer.echo(f"Mentor Model Provider: {metadata.get('mentor_model_provider') or 'none'}")
|
|
2315
2739
|
typer.echo(f"artifact_count: {metadata.get('artifact_count')}")
|
|
2316
2740
|
|
|
@@ -2330,28 +2754,32 @@ def _record_auto_share_skip(bundle: Path, *, mode: str, reason: str) -> None:
|
|
|
2330
2754
|
|
|
2331
2755
|
def _handle_ecosystem_auto_share(status: dict, *, quiet: bool = False, json_progress: bool = False) -> None:
|
|
2332
2756
|
settings = get_settings()
|
|
2333
|
-
mode = settings.
|
|
2334
|
-
if mode == "
|
|
2757
|
+
mode = settings.training_contribution_mode
|
|
2758
|
+
if mode == "private_internal_only":
|
|
2335
2759
|
return
|
|
2336
2760
|
bundle = _bundle_from_status(status)
|
|
2337
2761
|
if bundle is None:
|
|
2338
2762
|
return
|
|
2339
2763
|
emit = not quiet and not json_progress
|
|
2340
|
-
if mode == "
|
|
2341
|
-
|
|
2342
|
-
if
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
typer.echo("")
|
|
2347
|
-
typer.echo(f"Ecosystem Auto-Share skipped - {reason}")
|
|
2348
|
-
typer.echo("Ecosystem Auto-Share is configured for:")
|
|
2349
|
-
typer.echo(str(readiness.get("repo_url") or _ecosystem_repo_url()))
|
|
2350
|
-
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:
|
|
2351
2770
|
typer.echo("")
|
|
2352
|
-
typer.echo("
|
|
2353
|
-
typer.echo(
|
|
2354
|
-
|
|
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
|
|
2355
2783
|
try:
|
|
2356
2784
|
result = _ecosystem_contribute_impl(bundle, auto_share_mode=mode)
|
|
2357
2785
|
except Exception as exc:
|
|
@@ -2359,77 +2787,23 @@ def _handle_ecosystem_auto_share(status: dict, *, quiet: bool = False, json_prog
|
|
|
2359
2787
|
_record_auto_share_skip(bundle, mode=mode, reason=reason)
|
|
2360
2788
|
if emit:
|
|
2361
2789
|
typer.echo("")
|
|
2362
|
-
typer.echo("Ecosystem
|
|
2790
|
+
typer.echo("Public Ecosystem upload skipped - package could not be prepared")
|
|
2363
2791
|
typer.echo(f"Reason: {reason}")
|
|
2364
|
-
typer.echo(f"
|
|
2792
|
+
typer.echo(f"Experience Compilation: {bundle}")
|
|
2793
|
+
typer.echo(f"Retry: apprentice ecosystem contribute {bundle}")
|
|
2365
2794
|
return
|
|
2366
2795
|
if emit:
|
|
2367
2796
|
typer.echo("")
|
|
2368
2797
|
if result["contribution_created"]:
|
|
2369
|
-
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']}")
|
|
2370
2801
|
else:
|
|
2371
2802
|
reason = result.get("skipped_reason") or "public contribution was not created"
|
|
2372
|
-
typer.echo(f"Ecosystem
|
|
2373
|
-
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}")
|
|
2374
2806
|
return
|
|
2375
|
-
if mode == "ask":
|
|
2376
|
-
try:
|
|
2377
|
-
submission_dir, package_zip, metadata = _create_ecosystem_submission(bundle)
|
|
2378
|
-
_gh_issue_body(metadata, submission_dir, package_zip)
|
|
2379
|
-
except Exception as exc:
|
|
2380
|
-
reason = redact_secrets(str(exc))
|
|
2381
|
-
_record_auto_share_skip(bundle, mode=mode, reason=reason)
|
|
2382
|
-
if emit:
|
|
2383
|
-
typer.echo("")
|
|
2384
|
-
typer.echo("Ecosystem Auto-Share skipped - safety check failed")
|
|
2385
|
-
typer.echo(f"Reason: {reason}")
|
|
2386
|
-
typer.echo(f"Bundle: {bundle}")
|
|
2387
|
-
return
|
|
2388
|
-
if not emit:
|
|
2389
|
-
_record_auto_share_skip(bundle, mode=mode, reason="ask mode requires interactive output")
|
|
2390
|
-
return
|
|
2391
|
-
while True:
|
|
2392
|
-
typer.echo("")
|
|
2393
|
-
typer.echo("Share this Contribution Bundle to the public ecosystem now?")
|
|
2394
|
-
typer.echo("")
|
|
2395
|
-
typer.echo("1. Share now")
|
|
2396
|
-
typer.echo("2. Skip")
|
|
2397
|
-
typer.echo("3. Show submission summary")
|
|
2398
|
-
choice = typer.prompt("Default", default="skip").strip().lower()
|
|
2399
|
-
if choice in {"1", "share", "share now", "yes", "y"}:
|
|
2400
|
-
readiness = _ecosystem_auto_share_readiness()
|
|
2401
|
-
if not readiness["ready"]:
|
|
2402
|
-
reason = str(readiness.get("reason") or "not ready")
|
|
2403
|
-
_record_auto_share_skip(bundle, mode=mode, reason=reason)
|
|
2404
|
-
typer.echo(f"Ecosystem Auto-Share skipped - {reason}")
|
|
2405
|
-
typer.echo("Ecosystem Auto-Share is configured for:")
|
|
2406
|
-
typer.echo(str(readiness.get("repo_url") or _ecosystem_repo_url()))
|
|
2407
|
-
if readiness.get("next_action"):
|
|
2408
|
-
typer.echo("")
|
|
2409
|
-
typer.echo("Run:")
|
|
2410
|
-
typer.echo(str(readiness["next_action"]))
|
|
2411
|
-
return
|
|
2412
|
-
try:
|
|
2413
|
-
result = _ecosystem_contribute_impl(bundle, auto_share_mode=mode)
|
|
2414
|
-
except Exception as exc:
|
|
2415
|
-
reason = redact_secrets(str(exc))
|
|
2416
|
-
_record_auto_share_skip(bundle, mode=mode, reason=reason)
|
|
2417
|
-
typer.echo("Ecosystem Auto-Share skipped - safety check failed")
|
|
2418
|
-
typer.echo(f"Reason: {reason}")
|
|
2419
|
-
typer.echo(f"Bundle: {bundle}")
|
|
2420
|
-
return
|
|
2421
|
-
if result["contribution_created"]:
|
|
2422
|
-
typer.echo(f"Public ecosystem contribution created: {result['contribution_url']}")
|
|
2423
|
-
else:
|
|
2424
|
-
typer.echo(f"Ecosystem Auto-Share skipped - {result.get('skipped_reason') or 'public contribution was not created'}")
|
|
2425
|
-
typer.echo(f"Share later: apprentice ecosystem contribute {bundle}")
|
|
2426
|
-
return
|
|
2427
|
-
if choice in {"3", "summary", "show", "show submission summary"}:
|
|
2428
|
-
_print_auto_share_summary(metadata)
|
|
2429
|
-
continue
|
|
2430
|
-
_record_auto_share_skip(bundle, mode=mode, reason="user skipped")
|
|
2431
|
-
typer.echo(f"Share later: apprentice ecosystem contribute {bundle}")
|
|
2432
|
-
return
|
|
2433
2807
|
|
|
2434
2808
|
|
|
2435
2809
|
def _gh_issue_body(metadata: dict, submission_dir: Path, package_zip: Path) -> Path:
|
|
@@ -2468,11 +2842,12 @@ def learn_search(query: str | None = typer.Argument(None, help="Search ecosystem
|
|
|
2468
2842
|
|
|
2469
2843
|
@learn_app.command("create")
|
|
2470
2844
|
def learn_create(
|
|
2471
|
-
sources: list[str] = typer.Argument(..., help="Ecosystem ids or local
|
|
2845
|
+
sources: list[str] = typer.Argument(..., help="Ecosystem ids or local Experience Compilation paths."),
|
|
2472
2846
|
title: str | None = typer.Option(None, "--title", help="Optional Experience Pack title."),
|
|
2473
2847
|
replay: bool = typer.Option(False, "--replay", help="Run before/after replay after creating the pack."),
|
|
2474
2848
|
runner: str | None = typer.Option(None, "--runner", help="Advanced compatibility runner override for replay."),
|
|
2475
|
-
|
|
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),
|
|
2476
2851
|
):
|
|
2477
2852
|
settings = get_settings()
|
|
2478
2853
|
resolved = [resolve_learning_source(source, settings) for source in sources]
|
|
@@ -2486,7 +2861,7 @@ def learn_create(
|
|
|
2486
2861
|
typer.echo(f"- apprentice learn preview {pack.get('pack_id')}")
|
|
2487
2862
|
typer.echo(f"- apprentice learn replay {pack.get('pack_id')}")
|
|
2488
2863
|
if replay:
|
|
2489
|
-
_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)
|
|
2490
2865
|
|
|
2491
2866
|
|
|
2492
2867
|
@learn_app.command("preview")
|
|
@@ -2514,8 +2889,8 @@ def learn_preview(pack_id: str = typer.Argument(..., help="Experience Pack id or
|
|
|
2514
2889
|
typer.echo(f"- {requirement}")
|
|
2515
2890
|
|
|
2516
2891
|
|
|
2517
|
-
def _learn_replay_impl(pack_id: str, *, runner: str | None,
|
|
2518
|
-
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)
|
|
2519
2894
|
_ensure_apprentice_agent_ready(settings, runner, interactive=False)
|
|
2520
2895
|
pack_path, pack = load_pack(pack_id, settings)
|
|
2521
2896
|
instruction = replay_instruction_for_pack(pack, settings)
|
|
@@ -2570,9 +2945,10 @@ def _learn_replay_impl(pack_id: str, *, runner: str | None, mentor_mode: str) ->
|
|
|
2570
2945
|
def learn_replay(
|
|
2571
2946
|
pack_id: str = typer.Argument(..., help="Experience Pack id or path."),
|
|
2572
2947
|
runner: str | None = typer.Option(None, "--runner", help="Advanced compatibility runner override."),
|
|
2573
|
-
|
|
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),
|
|
2574
2950
|
):
|
|
2575
|
-
_learn_replay_impl(pack_id, runner=runner,
|
|
2951
|
+
_learn_replay_impl(pack_id, runner=runner, apprenticeship_mode=mentor_mode or apprenticeship_mode)
|
|
2576
2952
|
|
|
2577
2953
|
|
|
2578
2954
|
@learn_app.command("keep")
|
|
@@ -2619,25 +2995,104 @@ def learn_status():
|
|
|
2619
2995
|
typer.echo(f"- {pack.get('pack_id')}: {pack.get('title')}")
|
|
2620
2996
|
|
|
2621
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
|
+
|
|
2622
3078
|
@ecosystem_app.command('guide')
|
|
2623
3079
|
def ecosystem_guide():
|
|
2624
3080
|
typer.echo('Agent Apprenticeship Ecosystem')
|
|
2625
3081
|
typer.echo('')
|
|
2626
|
-
typer.echo('The public ecosystem
|
|
2627
|
-
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:')
|
|
2628
3084
|
typer.echo(' apprentice ecosystem contribute <bundle_path>')
|
|
2629
3085
|
typer.echo('')
|
|
2630
|
-
typer.echo('Configure
|
|
2631
|
-
typer.echo(
|
|
2632
|
-
typer.echo(' apprentice ecosystem configure --
|
|
2633
|
-
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>')
|
|
2634
3089
|
typer.echo('')
|
|
2635
|
-
typer.echo('
|
|
2636
|
-
typer.echo(' apprentice ecosystem configure --
|
|
2637
|
-
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')
|
|
2638
3093
|
typer.echo('')
|
|
2639
|
-
typer.echo('
|
|
2640
|
-
typer.echo('
|
|
3094
|
+
typer.echo('Legacy GitHub issue sharing remains available only with:')
|
|
3095
|
+
typer.echo(' apprentice ecosystem contribute <bundle_path> --storage github')
|
|
2641
3096
|
typer.echo(f'Community Slack: {SLACK_LINK}')
|
|
2642
3097
|
|
|
2643
3098
|
|
|
@@ -2645,28 +3100,40 @@ def ecosystem_guide():
|
|
|
2645
3100
|
def ecosystem_configure(
|
|
2646
3101
|
repo: str | None=typer.Option(None, '--repo', help='Public ecosystem repo in owner/name form.'),
|
|
2647
3102
|
repo_path: Path | None=typer.Option(None, '--repo-path', help='Public ecosystem repo path for a local checkout/export.'),
|
|
2648
|
-
|
|
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.'),
|
|
2649
3113
|
):
|
|
2650
3114
|
settings = get_settings()
|
|
2651
|
-
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]):
|
|
2652
3116
|
status = _ecosystem_status()
|
|
2653
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}")
|
|
2654
3121
|
typer.echo(f"Current repo: {settings.ecosystem_repo or DEFAULT_PUBLIC_ECOSYSTEM_REPO}")
|
|
2655
3122
|
typer.echo(f"GitHub URL: {_ecosystem_repo_url(settings.ecosystem_repo)}")
|
|
2656
3123
|
typer.echo(f"Current public ecosystem repo path: {settings.ecosystem_repo_path or 'Not configured'}")
|
|
2657
3124
|
typer.echo(f"GitHub CLI installed: {_yes_no(status['gh_installed'])}")
|
|
2658
3125
|
typer.echo(f"GitHub CLI authenticated: {_yes_no(status['gh_authenticated'])}")
|
|
2659
|
-
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)}")
|
|
2660
3128
|
entered_repo = typer.prompt("Public ecosystem repo, owner/name, optional", default=settings.ecosystem_repo or DEFAULT_PUBLIC_ECOSYSTEM_REPO, show_default=False).strip()
|
|
2661
3129
|
if entered_repo:
|
|
2662
3130
|
repo = entered_repo
|
|
2663
3131
|
typer.echo("")
|
|
2664
|
-
typer.echo("Choose
|
|
2665
|
-
typer.echo("1.
|
|
2666
|
-
typer.echo("2.
|
|
2667
|
-
typer.
|
|
2668
|
-
|
|
2669
|
-
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)
|
|
2670
3137
|
updates = {}
|
|
2671
3138
|
candidate_repo = repo or settings.ecosystem_repo or DEFAULT_PUBLIC_ECOSYSTEM_REPO
|
|
2672
3139
|
if repo is not None:
|
|
@@ -2683,23 +3150,31 @@ def ecosystem_configure(
|
|
|
2683
3150
|
typer.echo(f"Public ecosystem repo path does not look like an Agent Apprenticeship ecosystem repo: {expanded}")
|
|
2684
3151
|
raise typer.Exit(1)
|
|
2685
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"
|
|
2686
3163
|
if auto_share is not None:
|
|
2687
|
-
|
|
2688
|
-
if
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
raise typer.Exit(1)
|
|
2702
|
-
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("/")
|
|
2703
3178
|
if not updates:
|
|
2704
3179
|
typer.echo("No ecosystem settings changed.")
|
|
2705
3180
|
return
|
|
@@ -2708,8 +3183,21 @@ def ecosystem_configure(
|
|
|
2708
3183
|
typer.echo(f"Public ecosystem repo configured: {repo}")
|
|
2709
3184
|
if repo_path is not None:
|
|
2710
3185
|
typer.echo(f"Public ecosystem repo path configured: {Path(repo_path).expanduser()}")
|
|
2711
|
-
if
|
|
2712
|
-
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}")
|
|
2713
3201
|
typer.echo("Next: apprentice ecosystem status")
|
|
2714
3202
|
|
|
2715
3203
|
|
|
@@ -2718,32 +3206,116 @@ def ecosystem_status_command():
|
|
|
2718
3206
|
status = _ecosystem_status()
|
|
2719
3207
|
settings = get_settings()
|
|
2720
3208
|
typer.echo("Public Ecosystem Status")
|
|
2721
|
-
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}")
|
|
2722
3216
|
typer.echo(f"GitHub URL: {status['repo_url']}")
|
|
2723
3217
|
typer.echo(f"Public ecosystem repo path: {status.get('repo_path') or 'Not configured'}")
|
|
2724
3218
|
if status.get("repo_path"):
|
|
2725
3219
|
typer.echo(f"Public ecosystem repo path exists: {_yes_no(status.get('repo_path_exists'))}")
|
|
2726
3220
|
typer.echo(f"GitHub CLI installed: {_yes_no(status['gh_installed'])}")
|
|
2727
3221
|
typer.echo(f"GitHub CLI authenticated: {_yes_no(status['gh_authenticated'])}")
|
|
2728
|
-
typer.echo(f"
|
|
2729
|
-
typer.echo(f"
|
|
2730
|
-
|
|
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":
|
|
2731
3227
|
readiness = _ecosystem_auto_share_readiness()
|
|
2732
|
-
typer.echo(f"Ecosystem
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
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}")
|
|
2737
3307
|
|
|
2738
3308
|
|
|
2739
3309
|
@ecosystem_app.command('contribute')
|
|
2740
3310
|
def ecosystem_contribute(
|
|
2741
|
-
bundle_path: Path=typer.Argument(..., help='
|
|
3311
|
+
bundle_path: Path=typer.Argument(..., help='Experience Compilation folder.'),
|
|
2742
3312
|
repo: str | None=typer.Option(None, '--repo', help='Public ecosystem repo in owner/name form.'),
|
|
2743
|
-
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.'),
|
|
2744
3316
|
):
|
|
2745
3317
|
try:
|
|
2746
|
-
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)
|
|
2747
3319
|
except Exception as exc:
|
|
2748
3320
|
typer.echo(f"Could not prepare public ecosystem contribution: {redact_secrets(str(exc))}")
|
|
2749
3321
|
raise typer.Exit(1)
|
|
@@ -2753,13 +3325,58 @@ def ecosystem_contribute(
|
|
|
2753
3325
|
metadata = result["metadata"]
|
|
2754
3326
|
target_repo = result["target_repo"]
|
|
2755
3327
|
issue_body = result["issue_body"]
|
|
2756
|
-
|
|
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.")
|
|
2757
3374
|
typer.echo("")
|
|
2758
3375
|
typer.echo(f"Bundle ID: {metadata.get('bundle_id')}")
|
|
2759
3376
|
typer.echo(f"Title: {metadata.get('title')}")
|
|
2760
|
-
typer.echo(f"Bundle: {bundle}")
|
|
2761
|
-
typer.echo(f"Submission package: {package_zip}")
|
|
2762
|
-
typer.echo(
|
|
3377
|
+
typer.echo(f"Bundle folder: {bundle.name}")
|
|
3378
|
+
typer.echo(f"Submission package: {package_zip.name}")
|
|
3379
|
+
typer.echo("Submission metadata: ecosystem_submission/ecosystem_submission.json")
|
|
2763
3380
|
if result["contribution_created"]:
|
|
2764
3381
|
typer.echo(f"Public contribution URL: {result['contribution_url']}")
|
|
2765
3382
|
typer.echo("No bundle files were uploaded automatically. Attach or submit the generated package according to the repo instructions.")
|
|
@@ -2769,9 +3386,9 @@ def ecosystem_contribute(
|
|
|
2769
3386
|
typer.echo(str(result.get("gh_error") or ""))
|
|
2770
3387
|
elif target_repo and _gh_path() and _gh_authenticated() and dry_run:
|
|
2771
3388
|
typer.echo("Dry run: no GitHub issue was created.")
|
|
2772
|
-
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")
|
|
2773
3390
|
else:
|
|
2774
|
-
typer.echo("No
|
|
3391
|
+
typer.echo("No GitHub issue was created.")
|
|
2775
3392
|
typer.echo(f"Public ecosystem: {_ecosystem_repo_url(target_repo)}")
|
|
2776
3393
|
if not _gh_path():
|
|
2777
3394
|
typer.echo("GitHub CLI `gh` is not installed.")
|
|
@@ -2780,19 +3397,31 @@ def ecosystem_contribute(
|
|
|
2780
3397
|
typer.echo("")
|
|
2781
3398
|
typer.echo("Manual public submission steps:")
|
|
2782
3399
|
typer.echo(f"1. Open {_ecosystem_repo_url(target_repo)} in GitHub.")
|
|
2783
|
-
typer.echo(
|
|
2784
|
-
typer.echo(f"3. Attach or share the submission package: {package_zip}")
|
|
3400
|
+
typer.echo("2. Create a new issue using: ecosystem_submission/GITHUB_ISSUE_BODY.md")
|
|
3401
|
+
typer.echo(f"3. Attach or share the submission package: {package_zip.name}")
|
|
2785
3402
|
typer.echo(f"Community Slack: {SLACK_LINK}")
|
|
2786
3403
|
|
|
2787
3404
|
|
|
2788
3405
|
@ecosystem_app.command('list')
|
|
2789
|
-
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
|
+
):
|
|
2790
3415
|
try:
|
|
2791
|
-
rows=_load_registry(registry)
|
|
3416
|
+
rows=_load_registry(registry, storage=storage)
|
|
2792
3417
|
except FileNotFoundError as exc:
|
|
2793
3418
|
typer.echo(str(exc))
|
|
2794
3419
|
raise typer.Exit(1)
|
|
2795
|
-
|
|
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)
|
|
2796
3425
|
|
|
2797
3426
|
|
|
2798
3427
|
@ecosystem_app.command('search')
|
|
@@ -2800,21 +3429,27 @@ def ecosystem_search(
|
|
|
2800
3429
|
query_arg: str | None=typer.Argument(None, help='Free-text query.'),
|
|
2801
3430
|
query: str | None=typer.Option(None, '--query', help='Free-text query.'),
|
|
2802
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.'),
|
|
2803
3434
|
title: str | None=typer.Option(None, '--title'),
|
|
2804
3435
|
domain: str | None=typer.Option(None, '--domain'),
|
|
2805
3436
|
subdomain: str | None=typer.Option(None, '--subdomain'),
|
|
2806
|
-
|
|
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),
|
|
2807
3439
|
task_status: str | None=typer.Option(None, '--task-status'),
|
|
2808
3440
|
run_status: str | None=typer.Option(None, '--run-status'),
|
|
2809
3441
|
min_traced_steps: int | None=typer.Option(None, '--min-traced-steps'),
|
|
2810
3442
|
max_traced_steps: int | None=typer.Option(None, '--max-traced-steps'),
|
|
2811
3443
|
expected_economic_value: str | None=typer.Option(None, '--expected-economic-value'),
|
|
3444
|
+
tag: str | None=typer.Option(None, '--tag'),
|
|
2812
3445
|
artifact_type: str | None=typer.Option(None, '--artifact-type'),
|
|
2813
3446
|
apprentice_agent: str | None=typer.Option(None, '--apprentice-agent'),
|
|
2814
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.'),
|
|
2815
3450
|
):
|
|
2816
3451
|
try:
|
|
2817
|
-
rows=_load_registry(registry)
|
|
3452
|
+
rows=_load_registry(registry, storage=storage)
|
|
2818
3453
|
except FileNotFoundError as exc:
|
|
2819
3454
|
typer.echo(str(exc))
|
|
2820
3455
|
raise typer.Exit(1)
|
|
@@ -2823,14 +3458,25 @@ def ecosystem_search(
|
|
|
2823
3458
|
filters.pop('query_arg', None)
|
|
2824
3459
|
filters.pop('query', None)
|
|
2825
3460
|
filters.pop('registry', None)
|
|
3461
|
+
filters.pop('storage', None)
|
|
2826
3462
|
filters.pop('effective_query', None)
|
|
2827
|
-
|
|
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)
|
|
2828
3467
|
|
|
2829
3468
|
|
|
2830
|
-
def _find_registry_row(bundle_id: str, registry: Path | None=None) -> dict:
|
|
2831
|
-
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)
|
|
2832
3471
|
for row in rows:
|
|
2833
|
-
|
|
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:
|
|
2834
3480
|
return row
|
|
2835
3481
|
raise FileNotFoundError(f'Bundle id not found in registry: {bundle_id}')
|
|
2836
3482
|
|
|
@@ -2850,16 +3496,113 @@ def _prepare_ecosystem_pull_destination(bundle_id: str) -> Path:
|
|
|
2850
3496
|
return dest
|
|
2851
3497
|
|
|
2852
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
|
+
|
|
2853
3573
|
@ecosystem_app.command('pull')
|
|
2854
3574
|
def ecosystem_pull(
|
|
2855
3575
|
bundle_id: str=typer.Argument(...),
|
|
2856
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.'),
|
|
2857
3582
|
):
|
|
2858
3583
|
try:
|
|
2859
|
-
row=_find_registry_row(bundle_id, registry)
|
|
3584
|
+
row=_find_registry_row(bundle_id, registry, storage=storage)
|
|
2860
3585
|
except FileNotFoundError as exc:
|
|
2861
3586
|
typer.echo(str(exc))
|
|
2862
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
|
|
2863
3606
|
src=row.get('local_bundle_path') or row.get('path') or row.get('bundle_path') or row.get('bundle_path_or_url')
|
|
2864
3607
|
if not src:
|
|
2865
3608
|
task_path = row.get("task_path") or row.get("task_packet_path")
|
|
@@ -2927,6 +3670,39 @@ def ecosystem_pull(
|
|
|
2927
3670
|
typer.echo('Next action: set AA_HOME to a writable directory and try again.')
|
|
2928
3671
|
raise typer.Exit(1)
|
|
2929
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}")
|
|
2930
3706
|
|
|
2931
3707
|
|
|
2932
3708
|
def _summarize_bundle_manifest(manifest: dict) -> str:
|
|
@@ -2934,14 +3710,16 @@ def _summarize_bundle_manifest(manifest: dict) -> str:
|
|
|
2934
3710
|
'bundle_id','title','attempts','traced_steps','process_supervision_rows',
|
|
2935
3711
|
'reward_modeling_rows','revision_preference_pairs','domains','subdomains',
|
|
2936
3712
|
'agent_apprentice_role','expected_economic_value',
|
|
2937
|
-
'expected_economic_value_for_agent_apprentice','
|
|
3713
|
+
'expected_economic_value_for_agent_apprentice','task_status','run_status'
|
|
2938
3714
|
]
|
|
2939
3715
|
lines = []
|
|
2940
3716
|
for key in keys:
|
|
2941
3717
|
if key not in manifest:
|
|
2942
3718
|
continue
|
|
2943
|
-
|
|
2944
|
-
|
|
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)))}")
|
|
2945
3723
|
return "\n".join(lines)
|
|
2946
3724
|
|
|
2947
3725
|
|
|
@@ -2966,21 +3744,23 @@ def _summarize_seed_registry_row(row: dict) -> str:
|
|
|
2966
3744
|
lines.append(f"domains: {row.get('domains')}")
|
|
2967
3745
|
if row.get("subdomains"):
|
|
2968
3746
|
lines.append(f"subdomains: {row.get('subdomains')}")
|
|
2969
|
-
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")
|
|
2970
3748
|
return "\n".join(lines)
|
|
2971
3749
|
|
|
2972
3750
|
|
|
2973
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 {}
|
|
2974
3755
|
lines = [
|
|
2975
|
-
"kind:
|
|
2976
|
-
f"
|
|
3756
|
+
f"kind: {row.get('kind') or 'agent_experience_package'}",
|
|
3757
|
+
f"item_id: {item_id}",
|
|
2977
3758
|
f"title: {row.get('title')}",
|
|
2978
3759
|
f"task_status: {row.get('task_status') or 'unknown'}",
|
|
2979
3760
|
f"run_status: {row.get('run_status') or 'unknown'}",
|
|
2980
3761
|
]
|
|
2981
3762
|
for key in [
|
|
2982
3763
|
"apprentice_agent",
|
|
2983
|
-
"mentor_mode",
|
|
2984
3764
|
"mentor_model_provider",
|
|
2985
3765
|
"attempts",
|
|
2986
3766
|
"traced_steps",
|
|
@@ -2992,26 +3772,119 @@ def _summarize_contribution_registry_row(row: dict) -> str:
|
|
|
2992
3772
|
"expected_economic_value_for_agent_apprentice",
|
|
2993
3773
|
]:
|
|
2994
3774
|
if row.get(key) is not None:
|
|
2995
|
-
|
|
2996
|
-
|
|
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)))}")
|
|
2997
3779
|
if row.get("domains"):
|
|
2998
3780
|
lines.append(f"domains: {row.get('domains')}")
|
|
2999
3781
|
if row.get("subdomains"):
|
|
3000
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
|
+
)
|
|
3001
3814
|
return "\n".join(lines)
|
|
3002
3815
|
|
|
3003
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
|
+
|
|
3004
3869
|
@ecosystem_app.command('inspect')
|
|
3005
3870
|
def ecosystem_inspect(
|
|
3006
3871
|
bundle_id: str=typer.Argument(...),
|
|
3007
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.'),
|
|
3008
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
|
|
3009
3879
|
bundle_path=get_settings().app_home / 'ecosystem' / 'bundles' / bundle_id
|
|
3010
3880
|
if bundle_path.exists() and (bundle_path/'contribution_manifest.json').exists():
|
|
3011
3881
|
typer.echo(_summarize_bundle_manifest(read_json(bundle_path/'contribution_manifest.json')))
|
|
3012
3882
|
return
|
|
3013
3883
|
try:
|
|
3014
|
-
|
|
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)
|
|
3015
3888
|
except FileNotFoundError as exc:
|
|
3016
3889
|
typer.echo(str(exc))
|
|
3017
3890
|
raise typer.Exit(1)
|
|
@@ -3022,10 +3895,12 @@ def ecosystem_inspect(
|
|
|
3022
3895
|
|
|
3023
3896
|
|
|
3024
3897
|
@bundle_app.command('inspect')
|
|
3025
|
-
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
|
+
|
|
3026
3901
|
manifest=path/'contribution_manifest.json'
|
|
3027
3902
|
if not manifest.exists():
|
|
3028
|
-
typer.echo(f'
|
|
3903
|
+
typer.echo(f'Experience Compilation manifest not found: {manifest}')
|
|
3029
3904
|
raise typer.Exit(1)
|
|
3030
3905
|
manifest_data=read_json(manifest)
|
|
3031
3906
|
artifacts=_artifact_index(path)
|
|
@@ -3041,6 +3916,15 @@ def bundle_inspect(path: Path=typer.Argument(..., help='Contribution Bundle fold
|
|
|
3041
3916
|
typer.echo(f"follow_up_count: {len(followups)}")
|
|
3042
3917
|
if checkpoints:
|
|
3043
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]}")
|
|
3044
3928
|
top=[row.get("package_relative_path") or row.get("artifact_ref") for row in artifacts[:5]]
|
|
3045
3929
|
if top:
|
|
3046
3930
|
typer.echo("top_artifacts:")
|
|
@@ -3051,14 +3935,16 @@ def bundle_inspect(path: Path=typer.Argument(..., help='Contribution Bundle fold
|
|
|
3051
3935
|
|
|
3052
3936
|
|
|
3053
3937
|
@bundle_app.command('check')
|
|
3054
|
-
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
|
+
|
|
3055
3941
|
issues: list[str] = []
|
|
3056
3942
|
bundle = Path(path).expanduser()
|
|
3057
3943
|
manifest_path = bundle / "contribution_manifest.json"
|
|
3058
3944
|
if not bundle.exists():
|
|
3059
3945
|
issues.append(f"Bundle path does not exist: {bundle}")
|
|
3060
3946
|
elif not manifest_path.exists():
|
|
3061
|
-
issues.append(f"
|
|
3947
|
+
issues.append(f"Experience Compilation manifest not found: {manifest_path}")
|
|
3062
3948
|
manifest_data = {}
|
|
3063
3949
|
if manifest_path.exists():
|
|
3064
3950
|
try:
|
|
@@ -3078,8 +3964,16 @@ def bundle_check(path: Path=typer.Argument(..., help='Contribution Bundle folder
|
|
|
3078
3964
|
issues.append(f"outputs/artifacts_index.json could not be parsed: {exc}")
|
|
3079
3965
|
for row in artifacts:
|
|
3080
3966
|
ref = row.get("package_relative_path") or row.get("artifact_ref")
|
|
3081
|
-
if ref
|
|
3082
|
-
|
|
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}")
|
|
3083
3977
|
for optional in ["session_events.jsonl", "progress_events.jsonl"]:
|
|
3084
3978
|
file_path = bundle / optional
|
|
3085
3979
|
if file_path.exists():
|
|
@@ -3087,44 +3981,110 @@ def bundle_check(path: Path=typer.Argument(..., help='Contribution Bundle folder
|
|
|
3087
3981
|
read_jsonl(file_path)
|
|
3088
3982
|
except Exception as exc:
|
|
3089
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
|
|
3090
4048
|
secret_hits = _bundle_secret_hits(bundle) if bundle.exists() else []
|
|
3091
4049
|
if secret_hits:
|
|
3092
4050
|
issues.append("Obvious secret-like values were found: " + ", ".join(secret_hits[:10]))
|
|
3093
4051
|
if issues:
|
|
3094
|
-
typer.echo("
|
|
3095
|
-
typer.echo(f"
|
|
4052
|
+
typer.echo("Experience Compilation check: needs attention")
|
|
4053
|
+
typer.echo(f"Experience Compilation: {bundle}")
|
|
3096
4054
|
for issue in issues:
|
|
3097
4055
|
typer.echo(f"Reason: {redact_secrets(issue)}")
|
|
3098
4056
|
typer.echo("Next action: fix the packaging issue, then rerun `apprentice bundle check`.")
|
|
3099
4057
|
raise typer.Exit(1)
|
|
3100
4058
|
status = manifest_data.get("task_status") or manifest_data.get("run_status") or "unknown"
|
|
3101
|
-
typer.echo("
|
|
3102
|
-
typer.echo(f"
|
|
4059
|
+
typer.echo("Experience Compilation check: passed")
|
|
4060
|
+
typer.echo(f"Experience Compilation: {bundle}")
|
|
3103
4061
|
typer.echo(f"Task status: {status}")
|
|
3104
|
-
|
|
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.")
|
|
3105
4065
|
|
|
3106
4066
|
|
|
3107
4067
|
@bundle_app.command('contribute')
|
|
3108
|
-
def bundle_contribute(path: Path=typer.Argument(..., help='
|
|
4068
|
+
def bundle_contribute(path: Path=typer.Argument(..., help='Experience Compilation folder.')):
|
|
3109
4069
|
manifest=path/'contribution_manifest.json'
|
|
3110
4070
|
if not manifest.exists():
|
|
3111
|
-
typer.echo(f'
|
|
4071
|
+
typer.echo(f'Experience Compilation manifest not found: {manifest}')
|
|
3112
4072
|
raise typer.Exit(1)
|
|
3113
4073
|
try:
|
|
3114
4074
|
submission_dir, package_zip, metadata = _create_ecosystem_submission(path)
|
|
3115
4075
|
except Exception as exc:
|
|
3116
4076
|
typer.echo(f"Could not prepare public ecosystem contribution: {redact_secrets(str(exc))}")
|
|
3117
4077
|
raise typer.Exit(1)
|
|
3118
|
-
typer.echo('
|
|
4078
|
+
typer.echo('Experience Compilation ready.')
|
|
3119
4079
|
typer.echo('')
|
|
3120
4080
|
typer.echo(f"Bundle ID: {metadata.get('bundle_id')}")
|
|
3121
4081
|
typer.echo(f"Title: {metadata.get('title')}")
|
|
3122
|
-
typer.echo('
|
|
4082
|
+
typer.echo('Experience Compilation:')
|
|
3123
4083
|
typer.echo(str(path))
|
|
3124
4084
|
typer.echo('')
|
|
3125
4085
|
typer.echo('No upload was performed by this command.')
|
|
3126
|
-
typer.echo(f"Submission package: {package_zip}")
|
|
3127
|
-
typer.echo(
|
|
4086
|
+
typer.echo(f"Submission package: {package_zip.name}")
|
|
4087
|
+
typer.echo("Submission metadata: ecosystem_submission/ecosystem_submission.json")
|
|
3128
4088
|
typer.echo('')
|
|
3129
4089
|
typer.echo('Contribute to public ecosystem:')
|
|
3130
4090
|
typer.echo(f'apprentice ecosystem contribute {path}')
|