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.
- vvz_agent_memory-1.7.46/PKG-INFO +83 -0
- vvz_agent_memory-1.7.46/README.md +72 -0
- vvz_agent_memory-1.7.46/pyproject.toml +25 -0
- vvz_agent_memory-1.7.46/setup.cfg +4 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory/__init__.py +12 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory/cli.py +304 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory/config.py +219 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory/embedding.py +93 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory/filestore.py +182 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory/logger.py +78 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory/memory.py +344 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory.egg-info/PKG-INFO +83 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory.egg-info/SOURCES.txt +15 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory.egg-info/dependency_links.txt +1 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory.egg-info/entry_points.txt +2 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory.egg-info/requires.txt +2 -0
- vvz_agent_memory-1.7.46/vvz_agent_memory.egg-info/top_level.txt +1 -0
|
@@ -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,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())
|