agent-apprenticeship 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,25 +1,16 @@
1
1
  # Agent Apprenticeship
2
2
 
3
- The living ecosystem where AI agents learn from real-world work through iterative loops, reusable experience, and training-signal exchange.
3
+ The living ecosystem where AI agents learn from real-world work through iterative workflow loops, reusable experience, and collective training signal exchange.
4
4
 
5
- As agents move into long-horizon, economically valuable work, Agent Apprenticeship creates the open infrastructure where useful work generates reusable learning signals and challenging tasks improve through automated iterative loops.
5
+ As agents move into long-horizon, economically valuable work, Agent Apprenticeship creates the open infrastructure where real-world tasks generate reusable learning signals and complex workflows advance through agent loops that turn execution into shared improvement.
6
6
 
7
- Agent Apprenticeship is designed for an infinite exchange of work experience between agents: useful work creates training signals, signals improve future work, and future work creates new signals for the ecosystem.
7
+ Agent Apprenticeship is designed for a compounding exchange of agent work experience: economically valuable task execution generates training signals, those signals improve future work, and future work creates new reusable experience for the ecosystem.
8
8
 
9
- Agent Apprenticeship is built for loop iterations across domains, from simple tasks to complex specialized workflows. Apprentice agents can work with mentor agents to accomplish long-horizon, real-world tasks across model-assisted, expert-led, and hybrid modes, generating learning signals throughout the process.
9
+ Agent Apprenticeship is built for iterative workflow loops across domains, from simple tasks to complex specialized work. Apprentice agents work with mentor agents or human experts to complete long-horizon, real-world tasks, while each workflow generates reusable learning signals for the ecosystem.
10
10
 
11
- The first seed dataset includes:
11
+ Agent Apprenticeship is now available for anyone to start using with local agents including Codex, Cursor, Claude Code, OpenClaw, OpenCode, Hermes Agent, and custom agents, alongside different model providers. Users can run automated agent workflow loops locally, contribute agent learning signals back to the ecosystem, and use shared ecosystem signals to improve their own agents.
12
12
 
13
- * 500+ curated seed tasks sourced and grounded from real world
14
- * 495 reusable agent lessons
15
- * 1000+ full agent execution traces
16
- * 1000+ agent work episodes / task rollouts
17
-
18
- The seed dataset spans specialized economically valuable tasks across domains and forms the first layer of the Agent Apprenticeship ecosystem.
19
-
20
- Agent Apprenticeship is now available for anyone to start using with local agents including Codex, Cursor, Claude Code, OpenClaw, OpenCode, Hermes Agent, and custom agents, alongside different model providers. Users can experience automated iterative loops locally, contribute agent learning signals back to the ecosystem, and access ecosystem learning signals to improve their own agents.
21
-
22
- Agent Apprenticeship is also about the future of work and the economic value of agents. For every task executed through Agent Apprenticeship, the system can estimate task-level economic value, especially across specialized domains. It is built for everyday use to improve agent performance and outcome quality, while also enabling users to exchange agent work experience with each other and with domain-expert-led agents in one living ecosystem.
13
+ Agent Apprenticeship is about the future of work and the economic value of agents. For every task executed through Agent Apprenticeship, the system can estimate task-level economic value, especially across specialized domains. It is built for everyday use to improve agent performance and outcome quality, while enabling users to exchange agent work experience with each other and with domain-expert-led agents in one living ecosystem.
23
14
 
24
15
  ## Install
25
16
 
@@ -64,15 +64,15 @@ function findPython() {
64
64
  process.exit(1);
65
65
  }
66
66
  const candidates = [
67
- "python3.13",
68
- "python3.12",
69
67
  "python3.11",
70
- "/opt/homebrew/bin/python3.13",
71
- "/opt/homebrew/bin/python3.12",
68
+ "python3.12",
69
+ "python3.13",
72
70
  "/opt/homebrew/bin/python3.11",
73
- "/usr/local/bin/python3.13",
74
- "/usr/local/bin/python3.12",
71
+ "/opt/homebrew/bin/python3.12",
72
+ "/opt/homebrew/bin/python3.13",
75
73
  "/usr/local/bin/python3.11",
74
+ "/usr/local/bin/python3.12",
75
+ "/usr/local/bin/python3.13",
76
76
  "python3",
77
77
  "python"
78
78
  ];
@@ -88,17 +88,51 @@ function venvPython(venvDir) {
88
88
  : path.join(venvDir, "bin", "python");
89
89
  }
90
90
 
91
- function runQuiet(command, args, label) {
91
+ function runtimeEnv() {
92
+ const env = { ...process.env };
93
+ if (!env.PIP_CACHE_DIR) {
94
+ env.PIP_CACHE_DIR = path.join(appHome(), "pip-cache");
95
+ }
96
+ return env;
97
+ }
98
+
99
+ function setupTimeout(defaultMs) {
100
+ const raw = process.env.AA_NPM_SETUP_TIMEOUT_MS;
101
+ if (!raw) return defaultMs;
102
+ const value = Number(raw);
103
+ return Number.isFinite(value) && value > 0 ? value : defaultMs;
104
+ }
105
+
106
+ function runQuiet(command, args, label, timeoutMs) {
92
107
  const result = spawnSync(command, args, {
93
108
  encoding: "utf8",
94
109
  stdio: ["ignore", "pipe", "pipe"],
95
- env: process.env
110
+ env: runtimeEnv(),
111
+ timeout: setupTimeout(timeoutMs)
96
112
  });
97
113
  if (result.error || result.status !== 0) {
98
114
  console.error(`Agent Apprenticeship runtime setup failed while trying to ${label}.`);
99
- if (result.error) console.error(String(result.error.message || result.error));
115
+ const timedOut = Boolean(result.signal) || (result.error && String(result.error.message || result.error).includes("ETIMEDOUT"));
116
+ if (timedOut) {
117
+ console.error("Runtime setup timed out. Re-run the command, or set AA_PYTHON to a working Python 3.11+ interpreter.");
118
+ } else if (result.error) {
119
+ const message = String(result.error.message || result.error);
120
+ console.error(message);
121
+ }
100
122
  const out = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
101
- if (out) console.error(out);
123
+ if (out) console.error(out.slice(-4000));
124
+ process.exit(result.status || 1);
125
+ }
126
+ }
127
+
128
+ function runVisible(command, args, label) {
129
+ const result = spawnSync(command, args, {
130
+ stdio: "inherit",
131
+ env: runtimeEnv()
132
+ });
133
+ if (result.error || result.status !== 0) {
134
+ console.error(`Agent Apprenticeship runtime setup failed while trying to ${label}.`);
135
+ if (result.error) console.error(String(result.error.message || result.error));
102
136
  process.exit(result.status || 1);
103
137
  }
104
138
  }
@@ -117,10 +151,20 @@ function ensureRuntime(python) {
117
151
  }
118
152
 
119
153
  console.error("Installing Agent Apprenticeship runtime...");
154
+ console.error("First run can take a few minutes while Python dependencies are installed.");
120
155
  fs.rmSync(venvDir, { recursive: true, force: true });
121
156
  fs.mkdirSync(path.dirname(venvDir), { recursive: true });
122
- runQuiet(python, ["-m", "venv", venvDir], "create the Python environment");
123
- runQuiet(py, ["-m", "pip", "install", "--disable-pip-version-check", "--no-input", "--quiet", packageRoot], "install the Python package");
157
+ runQuiet(python, ["-m", "venv", venvDir], "create the Python environment", 120000);
158
+ runQuiet(py, [
159
+ "-m", "pip", "install",
160
+ "-q",
161
+ "--disable-pip-version-check",
162
+ "--progress-bar", "off",
163
+ "--no-input",
164
+ "--retries", "10",
165
+ "--timeout", "60",
166
+ packageRoot
167
+ ], "install the Python package", 600000);
124
168
  fs.writeFileSync(markerPath, JSON.stringify({
125
169
  packageName: packageJson.name,
126
170
  packageVersion: packageJson.version,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-apprenticeship",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "The living ecosystem for AI agents learning from real-world work through iterative loops and training-signal exchange.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "agent-apprenticeship"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "Open framework for turning real agent work into transferable agent experience"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,2 +1,2 @@
1
1
  """Agent Apprenticeship framework."""
2
- __version__ = "0.1.1"
2
+ __version__ = "0.1.2"
@@ -14,6 +14,7 @@ from .codex_runner import (
14
14
  _attempt_dir,
15
15
  _copy_attempt_inputs,
16
16
  _deliverables,
17
+ _run_with_process_group_timeout,
17
18
  ensure_attempt_outputs,
18
19
  )
19
20
  from .config import get_settings
@@ -271,7 +272,7 @@ def run_external_agent_attempt(package_root: Path, raw: RawTaskRecord, spec: Tas
271
272
  run_error = RuntimeError(invocation.unsupported_reason)
272
273
  else:
273
274
  try:
274
- cp = subprocess.run(invocation.argv, cwd=d, text=True, capture_output=True, timeout=timeout or settings.task_timeout_seconds)
275
+ cp = _run_with_process_group_timeout(invocation.argv, cwd=d, timeout=timeout or settings.task_timeout_seconds)
275
276
  returncode = cp.returncode
276
277
  stdout = cp.stdout or ""
277
278
  stderr = cp.stderr or ""
@@ -8,7 +8,7 @@ from typing import Any
8
8
 
9
9
  from .config import Settings, get_settings, normalize_mentor_mode
10
10
  from .io import append_jsonl, read_json, read_jsonl, write_json
11
- from .public_sanitizer import sanitize_public_obj
11
+ from .public_sanitizer import sanitize_public_obj, sanitize_public_text
12
12
  from .release_exporter import create_release
13
13
 
14
14
 
@@ -50,6 +50,30 @@ def _copy_json_if_exists(src: Path, dst: Path) -> bool:
50
50
  return True
51
51
 
52
52
 
53
+ def _copy_public_file(src: Path, dst: Path) -> bool:
54
+ if not src.exists() or not src.is_file():
55
+ return False
56
+ dst.parent.mkdir(parents=True, exist_ok=True)
57
+ if src.suffix == ".json":
58
+ try:
59
+ write_json(dst, _sanitize_bundle_json(read_json(src)))
60
+ except Exception:
61
+ dst.write_text(sanitize_public_text(src.read_text(errors="replace")) or "")
62
+ elif src.suffix == ".jsonl":
63
+ try:
64
+ dst.write_text("")
65
+ for row in read_jsonl(src):
66
+ append_jsonl(dst, sanitize_public_obj(row) if isinstance(row, dict) else row)
67
+ except Exception:
68
+ dst.write_text(sanitize_public_text(src.read_text(errors="replace")) or "")
69
+ else:
70
+ try:
71
+ dst.write_text(sanitize_public_text(src.read_text(errors="replace")) or "")
72
+ except UnicodeDecodeError:
73
+ shutil.copy2(src, dst)
74
+ return True
75
+
76
+
53
77
  def _run_status(release_root: Path) -> str:
54
78
  rows = read_jsonl(release_root / "packages_index.jsonl")
55
79
  statuses = [row.get("task_status") for row in rows if row.get("task_status")]
@@ -73,10 +97,10 @@ def contribution_counts(release_root: Path) -> dict[str, int]:
73
97
  def _write_card(bundle_root: Path) -> None:
74
98
  (bundle_root / "contribution_card.md").write_text(
75
99
  "# Agent Apprenticeship Contribution Bundle\n\n"
76
- "This local bundle packages an apprenticeship session for review and later contribution. "
100
+ "This local bundle packages an apprenticeship session for public ecosystem contribution. "
77
101
  "It includes session events, task instructions, traces, actual outputs, evaluation results, "
78
102
  "and learning-data views when available.\n\n"
79
- "No automatic upload is performed. Review this folder locally before sharing.\n\n"
103
+ "No automatic upload is performed. Inspect this folder locally before sharing.\n\n"
80
104
  "To contribute, submit the bundle to the Agent Apprenticeship ecosystem repo, "
81
105
  "or get help in Slack: https://join.slack.com/t/fsycommunity/shared_invite/zt-37417grrb-jFD6BQIYgC5wEMrW2bHssw\n"
82
106
  )
@@ -128,6 +152,7 @@ def _copy_clean_bundle_files(release_root: Path, bundle_root: Path, include_debu
128
152
 
129
153
  _copy_jsonl_if_nonempty(release_root / "actual_outputs.jsonl", bundle_root / "outputs" / "actual_outputs.jsonl")
130
154
  _copy_json_if_exists(release_root / "artifacts_index.json", bundle_root / "outputs" / "artifacts_index.json")
155
+ _copy_indexed_package_files(release_root, bundle_root)
131
156
 
132
157
  for name in [
133
158
  "grader_results.jsonl",
@@ -159,6 +184,41 @@ def _copy_clean_bundle_files(release_root: Path, bundle_root: Path, include_debu
159
184
  shutil.rmtree(bundle_root / "debug")
160
185
 
161
186
 
187
+ def _copy_indexed_package_files(release_root: Path, bundle_root: Path) -> None:
188
+ index_path = release_root / "artifacts_index.json"
189
+ if not index_path.exists():
190
+ return
191
+ raw_rows = read_json(index_path)
192
+ if not isinstance(raw_rows, list):
193
+ return
194
+ task_ids = sorted({str(row.get("task_id")) for row in raw_rows if isinstance(row, dict) and row.get("task_id")})
195
+ multiple_tasks = len(task_ids) > 1
196
+ rewritten: list[dict[str, Any]] = []
197
+ for row in raw_rows:
198
+ if not isinstance(row, dict):
199
+ continue
200
+ rel = str(row.get("package_relative_path") or "")
201
+ task_id = str(row.get("task_id") or "")
202
+ if not rel or not task_id or is_unsafe_bundle_ref(rel):
203
+ rewritten.append(sanitize_public_obj(row))
204
+ continue
205
+ src = release_root / "packages" / task_id / rel
206
+ bundle_rel = f"packages/{task_id}/{rel}" if multiple_tasks else rel
207
+ clean_row = sanitize_public_obj(dict(row))
208
+ clean_row["package_relative_path"] = bundle_rel
209
+ if src.exists() and _copy_public_file(src, bundle_root / bundle_rel):
210
+ clean_row.pop("artifact_missing", None)
211
+ else:
212
+ clean_row["artifact_missing"] = True
213
+ rewritten.append(clean_row)
214
+ write_json(bundle_root / "outputs" / "artifacts_index.json", rewritten)
215
+
216
+
217
+ def is_unsafe_bundle_ref(ref: str) -> bool:
218
+ path = Path(ref)
219
+ return path.is_absolute() or ".." in path.parts
220
+
221
+
162
222
  def _first_jsonl_row(path: Path) -> dict[str, Any]:
163
223
  rows = read_jsonl(path) if path.exists() else []
164
224
  for row in rows: