vvz-agent-memory 1.7.46__tar.gz

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.
@@ -0,0 +1,83 @@
1
+ Metadata-Version: 2.4
2
+ Name: vvz-agent-memory
3
+ Version: 1.7.46
4
+ Summary: Agent dialogue memory for the vvz prompt contract: AI Logger, with a local .agent-memory/ file store while the logger is unreachable
5
+ Author-email: Vasiliy Zdanovskiy <vasilyvz@gmail.com>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: ailogger-client>=0.1.17
10
+ Requires-Dist: embed-client>=4.0.24
11
+
12
+ # vvz-agent-memory
13
+
14
+ `agent-memory` is how agents of a `file_access: local` project under the vvz prompt contract write
15
+ and read their dialogue messages. It wraps the published `ailogger-client`:
16
+
17
+ - while the AI Logger is reachable, every call goes to the logger;
18
+ - while it is unreachable (connection refused, connect timeout, DNS or transport failure, twice),
19
+ a write goes to `.agent-memory/<session_id>/` in the project root;
20
+ - `agent-memory import` later moves those messages into the logger with their original
21
+ `message_id` and time (`messages_import`, `occurred_at`), and moves the imported files to
22
+ `.agent-memory/.imported/`.
23
+
24
+ The package version equals the contract version (`1.7.46` serves contract `v1.7.46`).
25
+
26
+ ## The file store
27
+
28
+ One message is two files side by side; the name sorts by time (UTC):
29
+
30
+ ```
31
+ .agent-memory/<session_id>/2026-10-01T13.57.45.123-<UUID4>.msg the message (JSON, logger fields + relations)
32
+ .agent-memory/<session_id>/2026-10-01T13.57.45.123-<UUID4>.emb its vector: {message_id, model, dimension, embedding}
33
+ ```
34
+
35
+ The vector comes from the fleet embedding service through `embed_client`, with the logger's own
36
+ model (`BAAI/bge-m3`, 1024), so the closeness group works locally and survives the import. When the
37
+ embedder is down too, the `.msg` is written alone and `agent-memory embed` fills the `.emb` later.
38
+
39
+ Files are created once and never edited. There are no locks: the sequence number of a file message
40
+ is the count of messages already in the session, and references are resolved by `message_id`.
41
+ The directory must exist and be ignored by git (the prompt deployment does both); otherwise the
42
+ store is unavailable and nothing is written.
43
+
44
+ ## Configuration
45
+
46
+ One JSON document: `--config`, else `$AGENT_MEMORY_CONFIG`, else
47
+ `~/.config/vvz-agent-memory/config.json`. Sections `ailogger_client`, `embedding_client` (the fleet
48
+ client shape: `protocol`, `server.host/port`, `client.timeout`, `ssl.cert/key/ca/check_hostname`,
49
+ `auth.token/token_header`) and `embedding_model` (`model`, `dimension`). Relative paths resolve
50
+ against the document's directory.
51
+
52
+ ```
53
+ agent-memory config generate --output ~/.config/vvz-agent-memory/config.json \
54
+ --logger-host 192.168.254.26 --embed-host 192.168.254.26 \
55
+ --logger-cert client.crt --logger-key client.key --logger-ca ca.crt
56
+ agent-memory config validate
57
+ ```
58
+
59
+ ## Commands
60
+
61
+ Every command prints one JSON object: `{"success": true, "store": "logger|file|both", "data": …}`
62
+ in the logger's own data shape, or `{"success": false, "error": {"code", "message"}}`.
63
+ `--store auto|logger|file` (default `auto`) selects the side.
64
+
65
+ | Command | Logger call | File store |
66
+ |---|---|---|
67
+ | `write` | `append_message` | new `.msg` + `.emb` |
68
+ | `read --session-id --message-id [--sequence-number]` | `messages_read` block 1 (`message_get` finds the number) | the file ending in the id |
69
+ | `preview --session-id --sender-identity` | `messages_read` mode preview | files of that sender; both sides merged |
70
+ | `working-set --session-id (--anchor-message-id \| --anchor-text)` | `working_set` | last N up to the anchor + top M by cosine |
71
+ | `relations --session-id` | `relations_read` | relations of the files; both sides merged |
72
+ | `embed [--session-id]` | — | missing `.emb` files |
73
+ | `import [--session-id] [--batch-size] [--keep]` | `messages_import` | moved to `.imported/` after success |
74
+ | `config generate \| validate` | — | — |
75
+
76
+ Exit codes: 0 success; 1 logger refused or failed (or an import item failed); 2 usage; 3 file store
77
+ unavailable; 4 logger unreachable with no fallback; 5 not found; 6 configuration invalid;
78
+ 7 embedding unavailable where a vector is required.
79
+
80
+ ## Verification
81
+
82
+ `python -m pipeline` in this directory runs every check against the live logger and embedder;
83
+ `python -m pipeline <name>` runs one; `python -m pipeline --list` lists them.
@@ -0,0 +1,72 @@
1
+ # vvz-agent-memory
2
+
3
+ `agent-memory` is how agents of a `file_access: local` project under the vvz prompt contract write
4
+ and read their dialogue messages. It wraps the published `ailogger-client`:
5
+
6
+ - while the AI Logger is reachable, every call goes to the logger;
7
+ - while it is unreachable (connection refused, connect timeout, DNS or transport failure, twice),
8
+ a write goes to `.agent-memory/<session_id>/` in the project root;
9
+ - `agent-memory import` later moves those messages into the logger with their original
10
+ `message_id` and time (`messages_import`, `occurred_at`), and moves the imported files to
11
+ `.agent-memory/.imported/`.
12
+
13
+ The package version equals the contract version (`1.7.46` serves contract `v1.7.46`).
14
+
15
+ ## The file store
16
+
17
+ One message is two files side by side; the name sorts by time (UTC):
18
+
19
+ ```
20
+ .agent-memory/<session_id>/2026-10-01T13.57.45.123-<UUID4>.msg the message (JSON, logger fields + relations)
21
+ .agent-memory/<session_id>/2026-10-01T13.57.45.123-<UUID4>.emb its vector: {message_id, model, dimension, embedding}
22
+ ```
23
+
24
+ The vector comes from the fleet embedding service through `embed_client`, with the logger's own
25
+ model (`BAAI/bge-m3`, 1024), so the closeness group works locally and survives the import. When the
26
+ embedder is down too, the `.msg` is written alone and `agent-memory embed` fills the `.emb` later.
27
+
28
+ Files are created once and never edited. There are no locks: the sequence number of a file message
29
+ is the count of messages already in the session, and references are resolved by `message_id`.
30
+ The directory must exist and be ignored by git (the prompt deployment does both); otherwise the
31
+ store is unavailable and nothing is written.
32
+
33
+ ## Configuration
34
+
35
+ One JSON document: `--config`, else `$AGENT_MEMORY_CONFIG`, else
36
+ `~/.config/vvz-agent-memory/config.json`. Sections `ailogger_client`, `embedding_client` (the fleet
37
+ client shape: `protocol`, `server.host/port`, `client.timeout`, `ssl.cert/key/ca/check_hostname`,
38
+ `auth.token/token_header`) and `embedding_model` (`model`, `dimension`). Relative paths resolve
39
+ against the document's directory.
40
+
41
+ ```
42
+ agent-memory config generate --output ~/.config/vvz-agent-memory/config.json \
43
+ --logger-host 192.168.254.26 --embed-host 192.168.254.26 \
44
+ --logger-cert client.crt --logger-key client.key --logger-ca ca.crt
45
+ agent-memory config validate
46
+ ```
47
+
48
+ ## Commands
49
+
50
+ Every command prints one JSON object: `{"success": true, "store": "logger|file|both", "data": …}`
51
+ in the logger's own data shape, or `{"success": false, "error": {"code", "message"}}`.
52
+ `--store auto|logger|file` (default `auto`) selects the side.
53
+
54
+ | Command | Logger call | File store |
55
+ |---|---|---|
56
+ | `write` | `append_message` | new `.msg` + `.emb` |
57
+ | `read --session-id --message-id [--sequence-number]` | `messages_read` block 1 (`message_get` finds the number) | the file ending in the id |
58
+ | `preview --session-id --sender-identity` | `messages_read` mode preview | files of that sender; both sides merged |
59
+ | `working-set --session-id (--anchor-message-id \| --anchor-text)` | `working_set` | last N up to the anchor + top M by cosine |
60
+ | `relations --session-id` | `relations_read` | relations of the files; both sides merged |
61
+ | `embed [--session-id]` | — | missing `.emb` files |
62
+ | `import [--session-id] [--batch-size] [--keep]` | `messages_import` | moved to `.imported/` after success |
63
+ | `config generate \| validate` | — | — |
64
+
65
+ Exit codes: 0 success; 1 logger refused or failed (or an import item failed); 2 usage; 3 file store
66
+ unavailable; 4 logger unreachable with no fallback; 5 not found; 6 configuration invalid;
67
+ 7 embedding unavailable where a vector is required.
68
+
69
+ ## Verification
70
+
71
+ `python -m pipeline` in this directory runs every check against the live logger and embedder;
72
+ `python -m pipeline <name>` runs one; `python -m pipeline --list` lists them.
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vvz-agent-memory"
7
+ dynamic = ["version"]
8
+ description = "Agent dialogue memory for the vvz prompt contract: AI Logger, with a local .agent-memory/ file store while the logger is unreachable"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = "MIT"
12
+ authors = [{ name = "Vasiliy Zdanovskiy", email = "vasilyvz@gmail.com" }]
13
+ dependencies = [
14
+ "ailogger-client>=0.1.17",
15
+ "embed-client>=4.0.24",
16
+ ]
17
+
18
+ [project.scripts]
19
+ agent-memory = "vvz_agent_memory.cli:main"
20
+
21
+ [tool.setuptools]
22
+ packages = ["vvz_agent_memory"]
23
+
24
+ [tool.setuptools.dynamic]
25
+ version = { attr = "vvz_agent_memory.__version__" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,12 @@
1
+ """vvz-agent-memory — the agent-memory CLI of the prompt-factory contract.
2
+
3
+ Agents of a `file_access: local` project write and read their dialogue messages
4
+ through this tool. It is a wrapper over `ailogger-client`: while the AI Logger is
5
+ reachable every call goes to it; while it is not, messages are kept in
6
+ `.agent-memory/<session_id>/` as `.msg` + `.emb` file pairs and are later moved
7
+ into the logger with their original identity and time by `agent-memory import`.
8
+
9
+ The package version equals the contract version it serves.
10
+ """
11
+
12
+ __version__ = "1.7.46"
@@ -0,0 +1,304 @@
1
+ """`agent-memory` — the command line.
2
+
3
+ Every command prints exactly one JSON object on stdout:
4
+ ``{"success": true, "store": ..., "data": ...}`` or ``{"success": false, "error": {"code", "message"}}``.
5
+
6
+ Exit codes: 0 success; 1 the logger refused or failed; 2 usage; 3 file store unavailable;
7
+ 4 logger unreachable (and no fallback); 5 not found; 6 configuration invalid;
8
+ 7 embedding service unavailable where a vector is required.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import asyncio
15
+ import json
16
+ import sys
17
+ import uuid
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from . import __version__, config as cfg
22
+ from .filestore import FileStore, FileStoreUnavailable, project_root
23
+ from .logger import LoggerFailure, LoggerUnreachable
24
+ from .memory import Memory, NotFound, Unavailable
25
+
26
+ KINDS = ("human", "model", "tool", "system")
27
+ RELATION_KINDS = ("reply", "influence", "tool_result")
28
+ EXIT = {"LOGGER_FAILURE": 1, "USAGE": 2, "FILE_STORE_UNAVAILABLE": 3, "LOGGER_UNREACHABLE": 4,
29
+ "NOT_FOUND": 5, "CONFIG_INVALID": 6, "EMBEDDING_UNAVAILABLE": 7}
30
+
31
+
32
+ def _uuid4(value: str) -> str:
33
+ try:
34
+ parsed = uuid.UUID(value)
35
+ except ValueError as exc:
36
+ raise argparse.ArgumentTypeError(f"not a UUID: {value!r}") from exc
37
+ if parsed.version != 4 or str(parsed) != value.lower():
38
+ raise argparse.ArgumentTypeError(f"not a canonical UUID4: {value!r}")
39
+ return str(parsed)
40
+
41
+
42
+ def _relation(value: str) -> dict[str, str]:
43
+ kind, _, target = value.partition(":")
44
+ if kind not in RELATION_KINDS:
45
+ raise argparse.ArgumentTypeError(f"relation kind must be one of {list(RELATION_KINDS)}: {value!r}")
46
+ return {"to_message_id": _uuid4(target), "kind": kind}
47
+
48
+
49
+ def _attributes(value: str) -> dict[str, Any]:
50
+ try:
51
+ parsed = json.loads(value)
52
+ except json.JSONDecodeError as exc:
53
+ raise argparse.ArgumentTypeError(f"--attributes is not JSON: {exc}") from exc
54
+ if not isinstance(parsed, dict):
55
+ raise argparse.ArgumentTypeError("--attributes must be a JSON object")
56
+ return parsed
57
+
58
+
59
+ def _non_negative(value: str) -> int:
60
+ number = int(value)
61
+ if number < 0:
62
+ raise argparse.ArgumentTypeError("must be >= 0")
63
+ return number
64
+
65
+
66
+ def _positive(value: str) -> int:
67
+ number = int(value)
68
+ if number <= 0:
69
+ raise argparse.ArgumentTypeError("must be > 0")
70
+ return number
71
+
72
+
73
+ def _batch(value: str) -> int:
74
+ number = _positive(value)
75
+ if number > 1000:
76
+ raise argparse.ArgumentTypeError("the logger imports at most 1000 messages per call")
77
+ return number
78
+
79
+
80
+ def _bool(value: str) -> bool:
81
+ if value.lower() in ("true", "yes", "1"):
82
+ return True
83
+ if value.lower() in ("false", "no", "0"):
84
+ return False
85
+ raise argparse.ArgumentTypeError("expected true or false")
86
+
87
+
88
+ def _global_options(parser: argparse.ArgumentParser, suppress: bool) -> None:
89
+ """The same options before and after the subcommand; the subcommand's copy only overrides when given."""
90
+
91
+ def default(value: Any) -> Any:
92
+ return argparse.SUPPRESS if suppress else value
93
+
94
+ parser.add_argument("--config", default=default(None),
95
+ help=f"configuration document (default ${cfg.ENV_VAR} or {cfg.DEFAULT_PATH})")
96
+ parser.add_argument("--root", default=default(None),
97
+ help="project root holding .agent-memory/ (default: the git top level of the cwd)")
98
+ parser.add_argument("--store", choices=("auto", "logger", "file"), default=default("auto"),
99
+ help="auto (default): logger, file store only while the logger is unreachable")
100
+
101
+
102
+ def build_parser() -> argparse.ArgumentParser:
103
+ parser = argparse.ArgumentParser(
104
+ prog="agent-memory",
105
+ description="Agent dialogue memory: AI Logger, with the .agent-memory/ file store while it is unreachable.",
106
+ )
107
+ parser.add_argument("--version", action="version", version=f"agent-memory {__version__}")
108
+ _global_options(parser, suppress=False)
109
+ common = argparse.ArgumentParser(add_help=False)
110
+ _global_options(common, suppress=True)
111
+
112
+ class _Sub(argparse._SubParsersAction): # every subcommand also takes the global options
113
+ def add_parser(self, name: str, **kwargs: Any) -> argparse.ArgumentParser:
114
+ kwargs.setdefault("parents", [common])
115
+ return super().add_parser(name, **kwargs)
116
+
117
+ parser.register("action", "parsers", _Sub)
118
+ sub = parser.add_subparsers(dest="command", required=True)
119
+
120
+ w = sub.add_parser("write", help="append one message (append_message)")
121
+ w.add_argument("--session-id", required=True, type=_uuid4)
122
+ text = w.add_mutually_exclusive_group(required=True)
123
+ text.add_argument("--text")
124
+ text.add_argument("--text-file", help="file holding the text; '-' reads stdin")
125
+ w.add_argument("--sender-kind", required=True, choices=KINDS)
126
+ w.add_argument("--sender-identity", required=True)
127
+ w.add_argument("--receiver-kind", required=True, choices=KINDS)
128
+ w.add_argument("--receiver-identity", required=True)
129
+ w.add_argument("--reply-to", type=_uuid4, action="append", default=[], metavar="MESSAGE_ID",
130
+ help="a reply relation to this message (repeatable)")
131
+ w.add_argument("--relation", type=_relation, action="append", default=[], metavar="KIND:MESSAGE_ID",
132
+ help=f"a relation, kind one of {list(RELATION_KINDS)} (repeatable)")
133
+ w.add_argument("--attributes", type=_attributes, help="free-form JSON object")
134
+ w.add_argument("--priority", type=_non_negative)
135
+ w.add_argument("--corrects", type=_uuid4, metavar="MESSAGE_ID", help="the message this one corrects")
136
+ w.add_argument("--message-id", type=_uuid4, help="identifier to accept the message under (a retry)")
137
+
138
+ r = sub.add_parser("read", help="read one referenced message")
139
+ r.add_argument("--session-id", required=True, type=_uuid4)
140
+ r.add_argument("--message-id", required=True, type=_uuid4)
141
+ r.add_argument("--sequence-number", type=_non_negative)
142
+
143
+ p = sub.add_parser("preview", help="messages of one sender, one character of text each")
144
+ p.add_argument("--session-id", required=True, type=_uuid4)
145
+ p.add_argument("--sender-identity", required=True)
146
+ p.add_argument("--page-size", type=_positive, default=100)
147
+
148
+ ws = sub.add_parser("working-set", help="recent and closest messages around an anchor")
149
+ ws.add_argument("--session-id", required=True, type=_uuid4)
150
+ anchor = ws.add_mutually_exclusive_group(required=True)
151
+ anchor.add_argument("--anchor-message-id", type=_uuid4)
152
+ anchor.add_argument("--anchor-text")
153
+ ws.add_argument("--recency-count", type=_non_negative, default=10)
154
+ ws.add_argument("--similarity-count", type=_non_negative, default=10)
155
+
156
+ rel = sub.add_parser("relations", help="declared relations of a session")
157
+ rel.add_argument("--session-id", required=True, type=_uuid4)
158
+
159
+ e = sub.add_parser("embed", help="compute the missing .emb files of the file store")
160
+ e.add_argument("--session-id", type=_uuid4, action="append", default=[])
161
+
162
+ i = sub.add_parser("import", help="move file-store messages into the logger with their ids and times")
163
+ i.add_argument("--session-id", type=_uuid4, action="append", default=[],
164
+ help="session to import (repeatable; default: every session in the store)")
165
+ i.add_argument("--batch-size", type=_batch, default=500)
166
+ i.add_argument("--keep", action="store_true",
167
+ help="leave imported files in place (default: move them to .agent-memory/.imported/)")
168
+
169
+ c = sub.add_parser("config", help="generate or validate the configuration document")
170
+ csub = c.add_subparsers(dest="config_command", required=True)
171
+ g = csub.add_parser("generate", help="write a configuration document; every setting has an argument")
172
+ g.add_argument("--output", required=True, help="path of the document to write")
173
+ g.add_argument("--force", action="store_true", help="overwrite an existing document")
174
+ for side, port in (("logger", 8008), ("embed", 8001)):
175
+ g.add_argument(f"--{side}-protocol", choices=cfg.PROTOCOLS, default="mtls")
176
+ g.add_argument(f"--{side}-host", required=True)
177
+ g.add_argument(f"--{side}-port", type=int, default=port)
178
+ g.add_argument(f"--{side}-timeout", type=float, default=cfg.DEFAULT_TIMEOUT)
179
+ g.add_argument(f"--{side}-cert")
180
+ g.add_argument(f"--{side}-key")
181
+ g.add_argument(f"--{side}-ca")
182
+ g.add_argument(f"--{side}-check-hostname", type=_bool)
183
+ g.add_argument(f"--{side}-token")
184
+ g.add_argument(f"--{side}-token-header")
185
+ g.add_argument("--embed-model", default="BAAI/bge-m3", help="must be the logger's embedding model")
186
+ g.add_argument("--embed-dimension", type=_positive, default=1024)
187
+ csub.add_parser("validate", help="validate the configuration document", parents=[common])
188
+ return parser
189
+
190
+
191
+ def _emit(payload: dict[str, Any]) -> None:
192
+ json.dump(payload, sys.stdout, ensure_ascii=False, default=str)
193
+ sys.stdout.write("\n")
194
+
195
+
196
+ def _fail(code: str, message: str, **extra: Any) -> int:
197
+ _emit({"success": False, "error": {"code": code, "message": message, **extra}})
198
+ return EXIT[code]
199
+
200
+
201
+ def _config_command(args: argparse.Namespace) -> int:
202
+ if args.config_command == "generate":
203
+ out = Path(args.output).expanduser().resolve()
204
+ if out.exists() and not args.force:
205
+ return _fail("USAGE", f"{out} exists; pass --force to overwrite")
206
+ document = cfg.generate(args)
207
+ errors = cfg.validate(document, out.parent)
208
+ if errors:
209
+ return _fail("CONFIG_INVALID", "the generated document is invalid; nothing written", errors=errors)
210
+ out.parent.mkdir(parents=True, exist_ok=True)
211
+ out.write_text(json.dumps(document, indent=2) + "\n", encoding="utf-8")
212
+ _emit({"success": True, "data": {"path": str(out)}})
213
+ return 0
214
+ try:
215
+ loaded = cfg.load(args.config)
216
+ except cfg.ConfigProblem as exc:
217
+ return _fail("CONFIG_INVALID", "configuration document is invalid", errors=exc.errors)
218
+ _emit({"success": True, "data": {"path": str(loaded.path), "errors": []}})
219
+ return 0
220
+
221
+
222
+ def _read_text(args: argparse.Namespace) -> str:
223
+ if args.text is not None:
224
+ return args.text
225
+ if args.text_file == "-":
226
+ return sys.stdin.read()
227
+ return Path(args.text_file).read_text(encoding="utf-8")
228
+
229
+
230
+ async def _run(args: argparse.Namespace, memory: Memory) -> dict[str, Any]:
231
+ if args.command == "write":
232
+ relations = [{"to_message_id": m, "kind": "reply"} for m in args.reply_to] + args.relation
233
+ text = _read_text(args)
234
+ if not text:
235
+ raise ValueError("the text is empty")
236
+ return await memory.write(
237
+ session_id=args.session_id, text=text, sender_kind=args.sender_kind,
238
+ sender_identity=args.sender_identity, receiver_kind=args.receiver_kind,
239
+ receiver_identity=args.receiver_identity, priority=args.priority, message_id=args.message_id,
240
+ relations=relations, corrects_message_id=args.corrects, attributes=args.attributes,
241
+ )
242
+ if args.command == "read":
243
+ return await memory.read(session_id=args.session_id, message_id=args.message_id,
244
+ sequence_number=args.sequence_number)
245
+ if args.command == "preview":
246
+ return await memory.preview(session_id=args.session_id, sender_identity=args.sender_identity,
247
+ page_size=args.page_size)
248
+ if args.command == "working-set":
249
+ return await memory.working_set(
250
+ session_id=args.session_id, anchor_message_id=args.anchor_message_id, anchor_text=args.anchor_text,
251
+ recency_count=args.recency_count, similarity_count=args.similarity_count)
252
+ if args.command == "relations":
253
+ return await memory.relations(session_id=args.session_id)
254
+ if args.command == "embed":
255
+ return await memory.embed_missing(session_ids=args.session_id)
256
+ if args.command == "import":
257
+ return await memory.import_sessions(session_ids=args.session_id, batch_size=args.batch_size,
258
+ keep=args.keep)
259
+ raise AssertionError(args.command)
260
+
261
+
262
+ def main(argv: list[str] | None = None) -> int:
263
+ args = build_parser().parse_args(argv)
264
+ if args.command == "config":
265
+ return _config_command(args)
266
+ try:
267
+ loaded = cfg.load(args.config)
268
+ except cfg.ConfigProblem as exc:
269
+ return _fail("CONFIG_INVALID", "configuration document is invalid", errors=exc.errors)
270
+ files: FileStore | None = None
271
+ problem: str | None = None
272
+ try:
273
+ files = FileStore.open(project_root(args.root))
274
+ except FileStoreUnavailable as exc:
275
+ problem = str(exc)
276
+ if args.store == "file" and files is None:
277
+ return _fail("FILE_STORE_UNAVAILABLE", problem or "file store unavailable")
278
+ if args.command in ("embed", "import") and files is None:
279
+ return _fail("FILE_STORE_UNAVAILABLE", problem or "file store unavailable")
280
+ try:
281
+ result = asyncio.run(_run(args, Memory(loaded, args.store, files, problem)))
282
+ except ValueError as exc:
283
+ return _fail("USAGE", str(exc))
284
+ except NotFound as exc:
285
+ return _fail("NOT_FOUND", str(exc))
286
+ except FileStoreUnavailable as exc:
287
+ return _fail("FILE_STORE_UNAVAILABLE", str(exc))
288
+ except LoggerUnreachable as exc:
289
+ return _fail("LOGGER_UNREACHABLE", str(exc))
290
+ except Unavailable as exc:
291
+ return _fail("LOGGER_UNREACHABLE", str(exc), sides=exc.reasons)
292
+ except LoggerFailure as exc:
293
+ return _fail("LOGGER_FAILURE", str(exc), logger_code=exc.code)
294
+ code = 0
295
+ if args.command == "import" and any(s["failed"] for s in result["data"]["sessions"]):
296
+ code = EXIT["LOGGER_FAILURE"]
297
+ if args.command == "embed" and result["data"]["failed"]:
298
+ code = EXIT["EMBEDDING_UNAVAILABLE"]
299
+ _emit({"success": code == 0, **result})
300
+ return code
301
+
302
+
303
+ if __name__ == "__main__":
304
+ sys.exit(main())