From 04b09479abcddfe14e2932366cb0cd4e7234a907 Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:25:53 -0400 Subject: [PATCH 1/3] Add Noetica interaction artifact importer --- src/agent_term/noetica_import.py | 72 ++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/agent_term/noetica_import.py diff --git a/src/agent_term/noetica_import.py b/src/agent_term/noetica_import.py new file mode 100644 index 0000000..03a1b04 --- /dev/null +++ b/src/agent_term/noetica_import.py @@ -0,0 +1,72 @@ +"""Import Noetica SourceOSInteractionEvent artifact exports into AgentTerm.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +from agent_term.interaction import ( + interaction_to_agent_term_event, + load_interaction_event, + render_interaction_event, +) +from agent_term.store import EventStore + + +@dataclass(frozen=True) +class NoeticaImportResult: + """Result summary for an opt-in Noetica artifact import.""" + + imported: int + paths: tuple[Path, ...] + event_ids: tuple[str, ...] + + +def iter_noetica_interaction_artifacts(path: Path | str) -> list[Path]: + """Return sorted Noetica interaction artifact paths from a file or directory.""" + + root = Path(path) + if root.is_file(): + return [root] + if not root.exists(): + raise FileNotFoundError(f"Noetica interaction artifact path does not exist: {root}") + if not root.is_dir(): + raise ValueError(f"Noetica interaction artifact path must be a file or directory: {root}") + return sorted(p for p in root.glob("*.json") if p.is_file()) + + +def import_noetica_interaction_artifacts( + path: Path | str, + store: EventStore, + *, + channel: str = "!sourceos-interaction", + sender: str = "@agent-term", + render: bool = False, +) -> NoeticaImportResult: + """Record Noetica-exported SourceOSInteractionEvent JSON artifacts. + + This is deliberately pull/import based. AgentTerm remains an opt-in consumer and does + not become part of Noetica's default desktop execution path. + """ + + paths = iter_noetica_interaction_artifacts(path) + event_ids: list[str] = [] + + for artifact_path in paths: + interaction_event = load_interaction_event(artifact_path) + agent_term_event = interaction_to_agent_term_event( + interaction_event, + channel=channel, + sender=sender, + ) + store.append(agent_term_event) + event_ids.append(agent_term_event.event_id) + if render: + print(render_interaction_event(interaction_event)) + print(f"recorded: {agent_term_event.event_id}") + + return NoeticaImportResult( + imported=len(paths), + paths=tuple(paths), + event_ids=tuple(event_ids), + ) From de25506a3321da4f8a6235eec1f311ca7a7a566d Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:28:18 -0400 Subject: [PATCH 2/3] Add AgentTerm Noetica interaction import command --- src/agent_term/interaction_cli.py | 37 +++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/agent_term/interaction_cli.py b/src/agent_term/interaction_cli.py index 6530561..7a81940 100644 --- a/src/agent_term/interaction_cli.py +++ b/src/agent_term/interaction_cli.py @@ -11,6 +11,7 @@ load_interaction_event, render_interaction_event, ) +from agent_term.noetica_import import import_noetica_interaction_artifacts from agent_term.store import DEFAULT_DB_PATH, EventStore @@ -38,6 +39,15 @@ def build_parser() -> argparse.ArgumentParser: record.add_argument("--channel", default="!sourceos-interaction") record.add_argument("--sender", default="@agent-term") + import_noetica = subparsers.add_parser( + "import-noetica", + help="Import Noetica-exported SourceOSInteractionEvent JSON artifacts from a file or directory.", + ) + import_noetica.add_argument("path", type=Path) + import_noetica.add_argument("--channel", default="!sourceos-interaction") + import_noetica.add_argument("--sender", default="@agent-term") + import_noetica.add_argument("--render", action="store_true") + return parser @@ -64,6 +74,30 @@ def cmd_record(path: Path, db_path: Path, channel: str, sender: str) -> int: return 0 +def cmd_import_noetica( + path: Path, + db_path: Path, + channel: str, + sender: str, + render: bool, +) -> int: + store = EventStore(db_path) + try: + result = import_noetica_interaction_artifacts( + path, + store, + channel=channel, + sender=sender, + render=render, + ) + finally: + store.close() + print(f"imported: {result.imported}") + for event_id in result.event_ids: + print(f"recorded: {event_id}") + return 0 + + def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) @@ -74,6 +108,9 @@ def main(argv: list[str] | None = None) -> int: if args.command == "record": return cmd_record(args.path, Path(args.db), args.channel, args.sender) + if args.command == "import-noetica": + return cmd_import_noetica(args.path, Path(args.db), args.channel, args.sender, args.render) + parser.error(f"unknown command: {args.command}") return 2 From ea3cac7bc31747651084e693e68a3e68455f1e4a Mon Sep 17 00:00:00 2001 From: mdheller <21163552+mdheller@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:30:48 -0400 Subject: [PATCH 3/3] Test Noetica interaction artifact import --- tests/test_noetica_import.py | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tests/test_noetica_import.py diff --git a/tests/test_noetica_import.py b/tests/test_noetica_import.py new file mode 100644 index 0000000..5ac9c2c --- /dev/null +++ b/tests/test_noetica_import.py @@ -0,0 +1,65 @@ +"""Tests for opt-in Noetica interaction artifact import.""" + +from __future__ import annotations + +import shutil +from pathlib import Path + +from agent_term.noetica_import import ( + import_noetica_interaction_artifacts, + iter_noetica_interaction_artifacts, +) +from agent_term.store import EventStore + +FIXTURE = Path(__file__).parent / "fixtures" / "sourceos_interaction_event.json" + + +def test_iter_noetica_interaction_artifacts_accepts_single_file() -> None: + assert iter_noetica_interaction_artifacts(FIXTURE) == [FIXTURE] + + +def test_iter_noetica_interaction_artifacts_returns_sorted_json_files(tmp_path: Path) -> None: + a = tmp_path / "a.json" + b = tmp_path / "b.json" + ignored = tmp_path / "ignored.txt" + shutil.copyfile(FIXTURE, b) + shutil.copyfile(FIXTURE, a) + ignored.write_text("not-json", encoding="utf-8") + + assert iter_noetica_interaction_artifacts(tmp_path) == [a, b] + + +def test_import_noetica_interaction_artifacts_records_file(tmp_path: Path) -> None: + db_path = tmp_path / "events.sqlite3" + store = EventStore(db_path) + try: + result = import_noetica_interaction_artifacts(FIXTURE, store, channel="!demo") + recorded = store.tail(channel="!demo", limit=5) + finally: + store.close() + + assert result.imported == 1 + assert len(result.event_ids) == 1 + assert len(recorded) == 1 + assert recorded[0].kind == "sourceos_interaction" + assert recorded[0].source == "noetica" + assert recorded[0].metadata["sourceos_interaction_event_id"] == "urn:srcos:interaction-event:noetica-standalone-complete-0001" + + +def test_import_noetica_interaction_artifacts_records_directory(tmp_path: Path) -> None: + event_dir = tmp_path / "events" + event_dir.mkdir() + shutil.copyfile(FIXTURE, event_dir / "01.json") + shutil.copyfile(FIXTURE, event_dir / "02.json") + + db_path = tmp_path / "events.sqlite3" + store = EventStore(db_path) + try: + result = import_noetica_interaction_artifacts(event_dir, store, channel="!demo") + recorded = store.tail(channel="!demo", limit=5) + finally: + store.close() + + assert result.imported == 2 + assert len(result.event_ids) == 2 + assert len(recorded) == 2