jaros 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. jaros/__init__.py +6 -0
  2. jaros/cli.py +537 -0
  3. jaros/comms/__init__.py +22 -0
  4. jaros/comms/fs.py +137 -0
  5. jaros/comms/queue.py +113 -0
  6. jaros/core/__init__.py +24 -0
  7. jaros/core/decision.py +56 -0
  8. jaros/core/decision_gate.py +108 -0
  9. jaros/core/json_value.py +69 -0
  10. jaros/core/reasoning_boundary.py +27 -0
  11. jaros/daemon.py +527 -0
  12. jaros/eval/__init__.py +24 -0
  13. jaros/eval/runner.py +154 -0
  14. jaros/eval/suite.py +81 -0
  15. jaros/execution/__init__.py +16 -0
  16. jaros/execution/determinism.py +52 -0
  17. jaros/execution/executor.py +115 -0
  18. jaros/execution/handlers.py +88 -0
  19. jaros/execution/tools.py +101 -0
  20. jaros/harness/__init__.py +64 -0
  21. jaros/harness/capabilities.py +272 -0
  22. jaros/harness/harness.py +282 -0
  23. jaros/harness/rules.py +68 -0
  24. jaros/llm/__init__.py +20 -0
  25. jaros/llm/adapters/__init__.py +12 -0
  26. jaros/llm/adapters/default_adapter.py +29 -0
  27. jaros/llm/adapters/ollama_adapter.py +36 -0
  28. jaros/llm/adapters/uppercase_adapter.py +29 -0
  29. jaros/llm/client.py +83 -0
  30. jaros/llm/config.py +56 -0
  31. jaros/llm/factory.py +86 -0
  32. jaros/registry.py +163 -0
  33. jaros/runtime/__init__.py +23 -0
  34. jaros/runtime/agent_pool.py +173 -0
  35. jaros/runtime/agent_thread.py +122 -0
  36. jaros/runtime/lifecycle.py +51 -0
  37. jaros/scheduling/__init__.py +22 -0
  38. jaros/scheduling/cron.py +74 -0
  39. jaros/scheduling/scheduler.py +195 -0
  40. jaros/state/__init__.py +100 -0
  41. jaros/state/coordination.py +123 -0
  42. jaros/state/decision_log.py +346 -0
  43. jaros/state/log.py +175 -0
  44. jaros/state/machine.py +113 -0
  45. jaros/state/model.py +88 -0
  46. jaros/state/recover.py +103 -0
  47. jaros/state/swarm.py +243 -0
  48. jaros-0.1.0.dist-info/METADATA +358 -0
  49. jaros-0.1.0.dist-info/RECORD +53 -0
  50. jaros-0.1.0.dist-info/WHEEL +5 -0
  51. jaros-0.1.0.dist-info/entry_points.txt +2 -0
  52. jaros-0.1.0.dist-info/licenses/LICENSE +21 -0
  53. jaros-0.1.0.dist-info/top_level.txt +1 -0
jaros/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Jaros — a zero-infrastructure runtime that makes agent systems reproducible,
2
+ testable, and capability-safe by construction: a durable, crash-recoverable,
3
+ deterministically replayable state machine that orchestrates AI agents as
4
+ lightweight threads. See .jarify/PRIME-001 for the system intent."""
5
+
6
+ __version__ = "0.1.0"
jaros/cli.py ADDED
@@ -0,0 +1,537 @@
1
+ """Host Control CLI and shared-FS ingestion (EXT-008).
2
+
3
+ A cross-platform, pure-standard-library command-line client for driving a running
4
+ Jaros OS *from the host*. The entire interface between host and OS is the shared
5
+ data directory (a Docker-mounted volume): every command's effect is a read or a
6
+ write under that directory. No socket is ever opened and no network call is ever
7
+ made — the CLI reaches the daemon exclusively through files.
8
+
9
+ Commands::
10
+
11
+ jaros serve run the daemon (inside the container)
12
+ jaros submit <kind> [--input JSON] -> inbox/<id>.json
13
+ jaros add-agent <file.py> [--name K] -> agents/<name-or-file>.py
14
+ jaros status -> print status.json
15
+ jaros watch [--interval S] -> live status + new outbox results
16
+ jaros logs -> print the daemon log (if present)
17
+ jaros eval -> run the agent eval suite (evals/)
18
+ jaros replay [--json] -> reconstruct + verify a run (byte-identical, no model call)
19
+
20
+ global: --data-dir DIR (else $JAROS_DATA_DIR, else ./.jaros-data)
21
+
22
+ Writes are atomic (temp file + :func:`os.replace`), so the daemon never observes a
23
+ partial job or agent. This module lives directly under ``jaros/`` (not under an
24
+ agent package), so the structural comms / no-server checks correctly treat it as a
25
+ host orchestrator rather than an agent.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import argparse
31
+ import ast
32
+ import json
33
+ import os
34
+ import sys
35
+ import time
36
+ import uuid
37
+ from pathlib import Path
38
+ from typing import Sequence
39
+
40
+ #: Default shared data directory when neither --data-dir nor $JAROS_DATA_DIR is set.
41
+ DEFAULT_DATA_DIR = ".jaros-data"
42
+
43
+ #: Environment variable naming the shared data directory.
44
+ DATA_DIR_ENV = "JAROS_DATA_DIR"
45
+
46
+ #: Candidate daemon log file locations searched (relative to the data dir).
47
+ LOG_CANDIDATES = ("daemon.log", "logs/daemon.log")
48
+
49
+
50
+ # #EXT-008-REQ-1 Start
51
+ def resolve_data_dir(args: argparse.Namespace) -> Path:
52
+ """Resolve the shared data directory using ``pathlib`` only.
53
+
54
+ Preference order: the ``--data-dir`` flag, then the ``$JAROS_DATA_DIR``
55
+ environment variable, then the ``./.jaros-data`` default. Uses no
56
+ platform-specific separators; the returned path is the same directory the
57
+ daemon uses.
58
+ """
59
+ chosen = getattr(args, "data_dir", None)
60
+ if not chosen:
61
+ chosen = os.environ.get(DATA_DIR_ENV)
62
+ if not chosen:
63
+ chosen = DEFAULT_DATA_DIR
64
+ return Path(chosen)
65
+ # #EXT-008-REQ-1 End
66
+
67
+
68
+ def _atomic_write(target: Path, data: str) -> None:
69
+ """Write ``data`` to ``target`` atomically via a temp file + ``os.replace``.
70
+
71
+ The temp file is created in the same directory so the rename is atomic on
72
+ Windows and POSIX alike; the daemon never sees a partial file.
73
+ """
74
+ target.parent.mkdir(parents=True, exist_ok=True)
75
+ tmp = target.with_name(f".tmp-{uuid.uuid4().hex}-{target.name}")
76
+ tmp.write_text(data, encoding="utf-8")
77
+ os.replace(tmp, target)
78
+
79
+
80
+ # #EXT-008-REQ-2 Start
81
+ def cmd_submit(kind: str, input_json: str | None, data_dir: Path) -> Path:
82
+ """Write a job descriptor ``{id, kind, input}`` into ``inbox/`` atomically.
83
+
84
+ The ``--input`` string (if given) must parse as JSON; on malformed JSON a
85
+ :class:`ValueError` is raised and *nothing* is written. The job id is a fresh
86
+ ``uuid4``; the file is written to ``inbox/.tmp-<id>`` then ``os.replace``-d to
87
+ ``inbox/<id>.json`` so the daemon never reads a partial job. Returns the path
88
+ of the created job file.
89
+ """
90
+ data_dir = Path(data_dir)
91
+ if input_json is None:
92
+ parsed_input: object = None
93
+ else:
94
+ try:
95
+ parsed_input = json.loads(input_json)
96
+ except json.JSONDecodeError as exc:
97
+ raise ValueError(f"--input is not valid JSON: {exc}") from exc
98
+
99
+ job_id = uuid.uuid4().hex
100
+ job = {"id": job_id, "kind": kind, "input": parsed_input}
101
+ target = data_dir / "inbox" / f"{job_id}.json"
102
+ # Validate-then-write: the bad-JSON path above never reaches here, so a
103
+ # rejected submission leaves the inbox untouched.
104
+ tmp = (data_dir / "inbox" / f".tmp-{job_id}")
105
+ tmp.parent.mkdir(parents=True, exist_ok=True)
106
+ tmp.write_text(json.dumps(job, indent=2, sort_keys=True), encoding="utf-8")
107
+ os.replace(tmp, target)
108
+ return target
109
+ # #EXT-008-REQ-2 End
110
+
111
+
112
+ # #EXT-008-REQ-3 Start
113
+ def _discover_kind(source: str) -> str | None:
114
+ """Statically read an agent module's top-level ``KIND`` string, if any.
115
+
116
+ Parses the source with :mod:`ast` (no import, so no agent side effects run on
117
+ the host) and returns the literal value of a module-level ``KIND = "..."``
118
+ assignment, or ``None`` when it is absent / not a string literal.
119
+ """
120
+ try:
121
+ tree = ast.parse(source)
122
+ except SyntaxError:
123
+ return None
124
+ for node in tree.body:
125
+ if isinstance(node, ast.Assign):
126
+ targets = node.targets
127
+ elif isinstance(node, ast.AnnAssign):
128
+ targets = [node.target] if node.target is not None else []
129
+ else:
130
+ continue
131
+ for target in targets:
132
+ if isinstance(target, ast.Name) and target.id == "KIND":
133
+ value = getattr(node, "value", None)
134
+ if isinstance(value, ast.Constant) and isinstance(value.value, str):
135
+ return value.value
136
+ return None
137
+
138
+
139
+ def cmd_add_agent(path: str, name: str | None, data_dir: Path) -> tuple[Path, str | None]:
140
+ """Install an agent module into the watched ``agents/`` folder.
141
+
142
+ Validates the source ``*.py`` exists and is readable, then copies it to
143
+ ``agents/.tmp-<file>`` and ``os.replace``-s it to
144
+ ``agents/<name-or-filename>.py`` so the daemon never loads a partial module.
145
+ The destination filename defaults to the source filename; ``name`` overrides
146
+ its stem. Returns ``(installed_path, discovered_kind)``.
147
+ """
148
+ source = Path(path)
149
+ if not source.is_file():
150
+ raise FileNotFoundError(f"source agent not found or not a file: {path}")
151
+ try:
152
+ content = source.read_text(encoding="utf-8")
153
+ except OSError as exc:
154
+ raise OSError(f"source agent is not readable: {path} ({exc})") from exc
155
+
156
+ if name:
157
+ filename = name if name.endswith(".py") else f"{name}.py"
158
+ else:
159
+ filename = source.name
160
+ target = data_dir / "agents" / filename
161
+ _atomic_write(target, content)
162
+ return target, _discover_kind(content)
163
+ # #EXT-008-REQ-3 End
164
+
165
+
166
+ # #EXT-008-REQ-4 Start
167
+ def _read_status(data_dir: Path) -> dict[str, object] | None:
168
+ """Return the parsed ``status.json`` or ``None`` when it is absent/unreadable."""
169
+ status_path = data_dir / "status.json"
170
+ if not status_path.is_file():
171
+ return None
172
+ try:
173
+ return json.loads(status_path.read_text(encoding="utf-8"))
174
+ except (OSError, json.JSONDecodeError):
175
+ return None
176
+
177
+
178
+ def cmd_status(data_dir: Path, stream=None) -> int:
179
+ """Read and pretty-print ``status.json`` (graceful message when absent).
180
+
181
+ Returns 0 when status was printed and 1 when no status file exists yet.
182
+ """
183
+ out = stream if stream is not None else sys.stdout
184
+ status_path = data_dir / "status.json"
185
+ if not status_path.is_file():
186
+ print(
187
+ f"no status available yet (no {status_path} — is the daemon running?)",
188
+ file=out,
189
+ )
190
+ return 1
191
+ status = _read_status(data_dir)
192
+ if status is None:
193
+ print(f"status file is present but unreadable: {status_path}", file=out)
194
+ return 1
195
+ print(json.dumps(status, indent=2, sort_keys=True), file=out)
196
+ return 0
197
+
198
+
199
+ def cmd_watch(data_dir: Path, interval: float, stream=None) -> int:
200
+ """Loop printing status + newly-appeared ``outbox/*.json`` until interrupted.
201
+
202
+ Reads purely from the shared FS: on each pass it prints the current status and
203
+ any ``outbox/`` result files that have appeared since the previous pass. Exits
204
+ cleanly (returns 0) on ``KeyboardInterrupt``. No socket or network is used.
205
+ """
206
+ out = stream if stream is not None else sys.stdout
207
+ outbox = data_dir / "outbox"
208
+ seen: set[str] = set()
209
+ try:
210
+ while True:
211
+ cmd_status(data_dir, stream=out)
212
+ if outbox.is_dir():
213
+ for result in sorted(outbox.glob("*.json")):
214
+ if result.name in seen:
215
+ continue
216
+ seen.add(result.name)
217
+ print(f"--- new result: outbox/{result.name} ---", file=out)
218
+ try:
219
+ print(result.read_text(encoding="utf-8"), file=out)
220
+ except OSError:
221
+ pass
222
+ time.sleep(max(interval, 0.0))
223
+ except KeyboardInterrupt:
224
+ print("watch stopped.", file=out)
225
+ return 0
226
+
227
+
228
+ def cmd_logs(data_dir: Path, stream=None) -> int:
229
+ """Print the daemon log file under the data dir, if one is present.
230
+
231
+ Searches the conventional log locations (``daemon.log``, ``logs/daemon.log``)
232
+ and prints the first that exists. Returns 0 when a log was printed, 1 when no
233
+ log file was found. Reads only the shared FS — no socket, no network.
234
+ """
235
+ out = stream if stream is not None else sys.stdout
236
+ for candidate in LOG_CANDIDATES:
237
+ log_path = data_dir / candidate
238
+ if log_path.is_file():
239
+ try:
240
+ print(log_path.read_text(encoding="utf-8"), end="", file=out)
241
+ except OSError as exc:
242
+ print(f"log present but unreadable: {log_path} ({exc})", file=out)
243
+ return 1
244
+ return 0
245
+ searched = ", ".join(str(data_dir / c) for c in LOG_CANDIDATES)
246
+ print(f"no daemon log found (looked for: {searched})", file=out)
247
+ return 1
248
+ # #EXT-008-REQ-4 End
249
+
250
+
251
+ # #EXT-013-REQ-4 Start
252
+ def cmd_eval(data_dir: Path, stream=None) -> int:
253
+ """Run the agent eval suite in ``<data>/evals`` and print a pass/fail report.
254
+
255
+ Assembles a deterministic eval environment from the data dir — built-in +
256
+ agents and the read-only/custom tool handlers — then runs every case
257
+ in ``evals/*.json``. Returns 0 iff all cases pass. Reads/loads only the shared
258
+ FS; no network.
259
+ """
260
+ out = stream if stream is not None else sys.stdout
261
+ from jaros.eval import load_cases, run_suite
262
+ from jaros.execution.tools import load_custom_tools
263
+ from jaros.llm import LlmConfig, create_llm_client
264
+ from jaros.registry import AgentRegistry, load_agents, register_builtins
265
+
266
+ llm = create_llm_client(LlmConfig(provider="default"))
267
+ registry = AgentRegistry()
268
+ register_builtins(registry, llm)
269
+ load_agents(registry, data_dir / "agents", llm)
270
+ load_custom_tools(data_dir / "tools") # register tool handlers for result checks
271
+
272
+ cases = load_cases(data_dir / "evals")
273
+ if not cases:
274
+ print(f"no eval cases found in {data_dir / 'evals'}", file=out)
275
+ return 1
276
+
277
+ report = run_suite(cases, registry)
278
+ for r in report.results:
279
+ print(f"[{'PASS' if r.passed else 'FAIL'}] {r.case}", file=out)
280
+ if not r.passed:
281
+ if r.error:
282
+ print(f" error: {r.error}", file=out)
283
+ for c in r.checks:
284
+ if not c.ok:
285
+ print(f" - {c.name}: {c.detail}", file=out)
286
+ print(f"\n{report.passed}/{report.total} eval cases passed", file=out)
287
+ return 0 if report.ok else 1
288
+ # #EXT-013-REQ-4 End
289
+
290
+
291
+ # #EXT-008-REQ-6 Start
292
+ def cmd_replay(data_dir: Path, *, as_json: bool = False, verbose: bool = False, stream=None) -> int:
293
+ """Reconstruct a run from the recorded decision log + verify it, no daemon.
294
+
295
+ Re-applies ``<data>/state/decisions.log`` through the deterministic executor —
296
+ constructing **no** ``LlmClient`` and making zero model calls — into a FRESH
297
+ temp sandbox (its own transition log + ``SharedFileSystem``), so nothing in
298
+ the live data dir is touched and re-running is safe. The same runtime handlers
299
+ are reused over the sandbox, so byte-identity is faithful.
300
+
301
+ Exit codes: ``0`` byte-identical (reproducible), ``1`` divergence detected,
302
+ ``2`` nothing to replay.
303
+ """
304
+ out = stream if stream is not None else sys.stdout
305
+ from jaros.state import replay_swarm
306
+
307
+ data_dir = Path(data_dir)
308
+ res = replay_swarm(data_dir)
309
+
310
+ if res.decisions == 0:
311
+ if as_json:
312
+ print(json.dumps({"decisions": 0, "ok": False, "reason": "empty"}), file=out)
313
+ else:
314
+ print(
315
+ f"nothing to replay: no recorded decisions in "
316
+ f"{data_dir / 'state' / 'decisions.log'} (run `jaros submit ...` first)",
317
+ file=out,
318
+ )
319
+ return 2
320
+
321
+ # #EXT-015-REQ-5 Start
322
+ by_agent = {t.source: t.decisions for t in res.by_agent}
323
+ attribution = None
324
+ if res.attribution is not None:
325
+ a = res.attribution
326
+ attribution = {"kind": a.kind, "index": a.index, "id": a.id, "source": a.source, "reason": a.reason}
327
+
328
+ report = {
329
+ "decisions": res.decisions,
330
+ "byAgent": by_agent,
331
+ "modelCalls": 0,
332
+ "finalState": res.final_state,
333
+ "byteIdentical": res.byte_identical,
334
+ "chainOk": res.chain_ok,
335
+ "attribution": attribution,
336
+ "ok": res.ok,
337
+ }
338
+ if as_json:
339
+ print(json.dumps(report), file=out)
340
+ return 0 if res.ok else 1
341
+
342
+ print(
343
+ f"replayed {res.decisions} recorded decisions across {len(res.by_agent)} "
344
+ f"agent(s) - model calls: 0",
345
+ file=out,
346
+ )
347
+ for t in res.by_agent:
348
+ print(f" {t.source:<18}{t.decisions} decision(s)", file=out)
349
+ print(f" reconstructed state : {res.final_state}", file=out)
350
+ _bi = "yes" if res.byte_identical else ("no" if not res.chain_ok else "NO - divergence detected")
351
+ print(f" byte-identical : {_bi}", file=out)
352
+ print(
353
+ f" tamper-evident chain: {'intact' if res.chain_ok else 'BROKEN - ' + (res.chain_reason or '')}",
354
+ file=out,
355
+ )
356
+ if res.attribution is not None:
357
+ a = res.attribution
358
+ label = "DIVERGENCE" if a.kind == "divergence" else "FAILURE"
359
+ print(f" attribution [{label}] : agent '{a.source}' produced decision #{a.index} ({a.id})", file=out)
360
+ print(f" reason: {a.reason}", file=out)
361
+ if res.ok and res.attribution is None:
362
+ print("reproducible: the whole swarm reconstructs byte-identically, with no model call.", file=out)
363
+ elif res.ok and res.attribution is not None:
364
+ print("reproduced byte-identically; a member's handoff failed - attributed to the exact agent above.", file=out)
365
+ elif not res.chain_ok:
366
+ print(
367
+ "TAMPERED: the decision log's hash chain is broken - the recorded account "
368
+ "was altered, so the run cannot be trusted or replayed. "
369
+ f"{res.chain_reason or ''}",
370
+ file=out,
371
+ )
372
+ else:
373
+ print("DIVERGENCE: replay did not reproduce the run byte-identically (a non-deterministic handler?).", file=out)
374
+ return 0 if res.ok else 1
375
+ # #EXT-015-REQ-5 End
376
+ # #EXT-008-REQ-6 End
377
+
378
+
379
+ # #EXT-008-REQ-1 Start
380
+ def _build_parser() -> argparse.ArgumentParser:
381
+ """Construct the argparse parser with ``--data-dir`` + subcommands.
382
+
383
+ ``--data-dir`` is accepted both before the subcommand (``jaros --data-dir D
384
+ status``) and after it (``jaros status --data-dir D``); the per-subcommand
385
+ occurrence wins when both are given.
386
+ """
387
+ data_dir_help = (
388
+ "shared data directory the daemon uses "
389
+ f"(else ${DATA_DIR_ENV}, else ./{DEFAULT_DATA_DIR})"
390
+ )
391
+ parser = argparse.ArgumentParser(
392
+ prog="jaros",
393
+ description="Host control CLI for a Jaros OS (shared-filesystem only).",
394
+ )
395
+ parser.add_argument("--data-dir", dest="data_dir", default=None, help=data_dir_help)
396
+ sub = parser.add_subparsers(dest="command", required=True)
397
+
398
+ def add_command(name: str, help_text: str) -> argparse.ArgumentParser:
399
+ p = sub.add_parser(name, help=help_text)
400
+ # SUPPRESS keeps the global --data-dir value when the flag is not
401
+ # repeated after the subcommand.
402
+ p.add_argument(
403
+ "--data-dir", dest="data_dir", default=argparse.SUPPRESS, help=data_dir_help
404
+ )
405
+ return p
406
+
407
+ add_command("serve", "run the daemon (used inside the container)")
408
+
409
+ p_submit = add_command("submit", "write a job descriptor into inbox/")
410
+ p_submit.add_argument("kind", help="agent kind that should handle the job")
411
+ p_submit.add_argument(
412
+ "--input", dest="input", default=None, help="job input as a JSON string"
413
+ )
414
+
415
+ p_add = add_command("add-agent", "install an agent module into agents/")
416
+ p_add.add_argument("path", help="path to the agent module (*.py)")
417
+ p_add.add_argument(
418
+ "--name", dest="name", default=None, help="override the installed kind/filename"
419
+ )
420
+
421
+ add_command("status", "read and print status.json")
422
+
423
+ p_watch = add_command("watch", "live status + new outbox results")
424
+ p_watch.add_argument(
425
+ "--interval",
426
+ dest="interval",
427
+ type=float,
428
+ default=1.0,
429
+ help="seconds between refreshes (default: 1.0)",
430
+ )
431
+
432
+ add_command("logs", "print the daemon log file if present")
433
+ add_command("eval", "run the agent eval suite in evals/")
434
+
435
+ p_replay = add_command("replay", "reconstruct + verify a run from the decision log")
436
+ p_replay.add_argument("--json", dest="as_json", action="store_true", help="emit a one-line JSON report")
437
+ p_replay.add_argument("--verbose", dest="verbose", action="store_true", help="show extra detail on divergence")
438
+ return parser
439
+
440
+
441
+ # #EXT-008-REQ-1 Start
442
+ def load_dotenv(env_path: Path | None = None) -> None:
443
+ """Load keys/values from a .env file into os.environ if it exists."""
444
+ if env_path is None:
445
+ env_path = Path(".env")
446
+ if not env_path.is_file():
447
+ return
448
+ try:
449
+ content = env_path.read_text(encoding="utf-8")
450
+ for line in content.splitlines():
451
+ line = line.strip()
452
+ if not line or line.startswith("#"):
453
+ continue
454
+ if "=" in line:
455
+ key, val = line.split("=", 1)
456
+ key = key.strip()
457
+ val = val.strip()
458
+ if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
459
+ val = val[1:-1]
460
+ if key:
461
+ os.environ.setdefault(key, val)
462
+ except Exception:
463
+ pass
464
+ # #EXT-008-REQ-1 End
465
+
466
+
467
+ def main(argv: Sequence[str] | None = None) -> int:
468
+ """CLI entry point: parse ``argv`` and dispatch to the matching command.
469
+
470
+ Returns a process exit code. Never opens a socket or makes a network call;
471
+ every command's effect is a read or write under the shared data directory.
472
+ """
473
+ load_dotenv()
474
+ parser = _build_parser()
475
+ args = parser.parse_args(argv)
476
+ data_dir = resolve_data_dir(args)
477
+
478
+ if args.command == "serve":
479
+ # Imported lazily so the lightweight host commands don't pull in the whole
480
+ # daemon dependency graph just to submit a job or read status.
481
+ from jaros.daemon import Daemon
482
+ from jaros.llm.config import resolve_llm_config
483
+
484
+ # The default LLM is config-driven (config/llm.json or JAROS_LLM_PROVIDER);
485
+ # every agent reaches it through the one LlmClient interface.
486
+ llm_config = resolve_llm_config(data_dir)
487
+ print(f"[jaros] default LLM provider: {llm_config.provider}", file=sys.stderr)
488
+ return Daemon(data_dir=data_dir, llm_config=llm_config).run()
489
+
490
+ if args.command == "submit":
491
+ try:
492
+ target = cmd_submit(args.kind, args.input, data_dir)
493
+ except ValueError as exc:
494
+ print(f"error: {exc}", file=sys.stderr)
495
+ return 2
496
+ job_id = target.stem
497
+ print(f"submitted job {job_id} -> {target}")
498
+ return 0
499
+
500
+ if args.command == "add-agent":
501
+ try:
502
+ target, kind = cmd_add_agent(args.path, args.name, data_dir)
503
+ except (FileNotFoundError, OSError) as exc:
504
+ print(f"error: {exc}", file=sys.stderr)
505
+ return 2
506
+ if kind:
507
+ print(f"installed agent -> {target} (registers kind {kind!r})")
508
+ else:
509
+ print(f"installed agent -> {target}")
510
+ return 0
511
+
512
+ if args.command == "status":
513
+ return cmd_status(data_dir)
514
+
515
+ if args.command == "watch":
516
+ return cmd_watch(data_dir, args.interval)
517
+
518
+ if args.command == "logs":
519
+ return cmd_logs(data_dir)
520
+
521
+ if args.command == "eval":
522
+ return cmd_eval(data_dir)
523
+
524
+ if args.command == "replay":
525
+ return cmd_replay(
526
+ data_dir,
527
+ as_json=getattr(args, "as_json", False),
528
+ verbose=getattr(args, "verbose", False),
529
+ )
530
+
531
+ parser.error(f"unknown command: {args.command!r}") # pragma: no cover
532
+ return 2 # pragma: no cover
533
+ # #EXT-008-REQ-1 End
534
+
535
+
536
+ if __name__ == "__main__": # pragma: no cover
537
+ raise SystemExit(main())
@@ -0,0 +1,22 @@
1
+ """Communication Fabric (EXT-006).
2
+
3
+ The only two sanctioned inter-agent channels: rigid typed queues and a shared
4
+ file system with a fixed layout. No direct agent-to-agent calls exist.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from jaros.comms.fs import (
10
+ LAYOUT_DIRS,
11
+ LayoutViolationError,
12
+ SharedFileSystem,
13
+ )
14
+ from jaros.comms.queue import Queue, QueueContractError
15
+
16
+ __all__ = [
17
+ "Queue",
18
+ "QueueContractError",
19
+ "SharedFileSystem",
20
+ "LayoutViolationError",
21
+ "LAYOUT_DIRS",
22
+ ]