loki-mode 6.82.0 → 6.83.0

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/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v6.82.0
6
+ # Loki Mode v6.83.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -269,4 +269,4 @@ The following features are documented in skill modules but not yet fully automat
269
269
  | Quality gates 3-reviewer system | Implemented (v5.35.0) | 5 specialist reviewers in `skills/quality-gates.md`; execution in run.sh |
270
270
  | Benchmarks (HumanEval, SWE-bench) | Infrastructure only | Runner scripts and datasets exist in `benchmarks/`; no published results |
271
271
 
272
- **v6.82.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
272
+ **v6.83.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.82.0
1
+ 6.83.0
@@ -72,6 +72,38 @@ COUNCIL_DONE_SIGNALS=0
72
72
  COUNCIL_TOTAL_DONE_SIGNALS=0
73
73
  COUNCIL_LAST_DIFF_HASH=""
74
74
 
75
+ #===============================================================================
76
+ # v6.83.0 Phase 1: Managed Agents memory augmentation (opt-in).
77
+ #
78
+ # When LOKI_MANAGED_AGENTS=true AND LOKI_MANAGED_MEMORY=true, this function
79
+ # pulls up to 3 related prior verdicts from the Claude Managed Agents store
80
+ # and writes them to a file the council prompt-assembly step appends as
81
+ # "RELATED PRIOR VERDICTS". 5s hard timeout so a slow/unreachable API can
82
+ # never block the council. Silent no-op when the flags are off.
83
+ #===============================================================================
84
+ council_augment_from_managed_memory() {
85
+ if [ "${LOKI_MANAGED_AGENTS:-false}" != "true" ] || \
86
+ [ "${LOKI_MANAGED_MEMORY:-false}" != "true" ]; then
87
+ return 0
88
+ fi
89
+ local target_dir="${TARGET_DIR:-.}"
90
+ local project_dir="${PROJECT_DIR:-$(pwd)}"
91
+ local out_file="$target_dir/.loki/managed/council-augment.txt"
92
+ mkdir -p "$target_dir/.loki/managed" 2>/dev/null || true
93
+ (
94
+ cd "$project_dir" 2>/dev/null && \
95
+ LOKI_TARGET_DIR="$target_dir" \
96
+ timeout 5 python3 -m memory.managed_memory.retrieve \
97
+ --query "completion-council verdict context" --top-k 3 \
98
+ > "$out_file" 2>/dev/null || true
99
+ ) || true
100
+ if [ -s "$out_file" ]; then
101
+ echo "RELATED PRIOR VERDICTS:"
102
+ cat "$out_file"
103
+ fi
104
+ return 0
105
+ }
106
+
75
107
  #===============================================================================
76
108
  # Initialization
77
109
  #===============================================================================
@@ -1366,6 +1398,11 @@ council_should_stop() {
1366
1398
  return 1
1367
1399
  fi
1368
1400
 
1401
+ # v6.83.0 Phase 1: silent no-op unless both managed flags are on.
1402
+ # Writes related prior verdicts into $TARGET_DIR/.loki/managed/council-augment.txt
1403
+ # which the council prompt assembly step can read and append to its prompt.
1404
+ council_augment_from_managed_memory >/dev/null 2>&1 || true
1405
+
1369
1406
  # Check circuit breaker first (stagnation detection)
1370
1407
  local circuit_triggered=false
1371
1408
  if council_circuit_breaker_triggered; then
@@ -1396,6 +1433,27 @@ council_should_stop() {
1396
1433
  # Store final council report
1397
1434
  council_write_report
1398
1435
 
1436
+ # v6.83.0 Phase 1: shadow-write the final council verdict to the
1437
+ # managed memory store. Backgrounded + silent; flags gate the work
1438
+ # inside the Python module so no-op when off.
1439
+ if [ "${LOKI_MANAGED_AGENTS:-false}" = "true" ] && \
1440
+ [ "${LOKI_MANAGED_MEMORY:-false}" = "true" ]; then
1441
+ local _verdict_file="$loki_dir/council/verdicts/iteration-$ITERATION_COUNT.json"
1442
+ if [ ! -f "$_verdict_file" ]; then
1443
+ # Fall back to the round vote file as the verdict payload.
1444
+ _verdict_file="$COUNCIL_STATE_DIR/votes/round-${ITERATION_COUNT}.json"
1445
+ fi
1446
+ if [ -f "$_verdict_file" ]; then
1447
+ (
1448
+ cd "${PROJECT_DIR:-$(pwd)}" 2>/dev/null && \
1449
+ LOKI_TARGET_DIR="$loki_dir/.." \
1450
+ timeout 15 python3 -m memory.managed_memory.shadow_write \
1451
+ --verdict "$_verdict_file" >/dev/null 2>&1 || true
1452
+ ) &
1453
+ disown 2>/dev/null || true
1454
+ fi
1455
+ fi
1456
+
1399
1457
  return 0 # STOP
1400
1458
  fi
1401
1459
 
package/autonomy/run.sh CHANGED
@@ -644,6 +644,19 @@ LOKI_SESSION_MODEL="${LOKI_SESSION_MODEL:-sonnet}"
644
644
  LOKI_LEGACY_TIER_SWITCHING="${LOKI_LEGACY_TIER_SWITCHING:-false}"
645
645
  export LOKI_SESSION_MODEL LOKI_LEGACY_TIER_SWITCHING
646
646
 
647
+ # Managed Agents (v6.83.0 Phase 1): opt-in integration with Claude Managed Agents
648
+ # Memory store backend. Parent flag gates everything; child flags gate features.
649
+ # Both off (default) => zero behavior change from v6.82.0.
650
+ LOKI_MANAGED_AGENTS="${LOKI_MANAGED_AGENTS:-false}"
651
+ LOKI_MANAGED_MEMORY="${LOKI_MANAGED_MEMORY:-false}"
652
+ export LOKI_MANAGED_AGENTS LOKI_MANAGED_MEMORY
653
+
654
+ # Fail-fast: child on with parent off is a misconfiguration.
655
+ if [ "$LOKI_MANAGED_MEMORY" = "true" ] && [ "$LOKI_MANAGED_AGENTS" != "true" ]; then
656
+ echo "ERROR: LOKI_MANAGED_MEMORY=true requires LOKI_MANAGED_AGENTS=true" >&2
657
+ exit 2
658
+ fi
659
+
647
660
  # Parallel Workflows (Git Worktrees)
648
661
  PARALLEL_MODE=${LOKI_PARALLEL_MODE:-false}
649
662
  MAX_WORKTREES=${LOKI_MAX_WORKTREES:-5}
@@ -7977,6 +7990,32 @@ try:
7977
7990
  except Exception as e:
7978
7991
  pass # Silently fail if memory not available
7979
7992
  PYEOF
7993
+
7994
+ # v6.83.0 Phase 1: RARV-C REASON augment. When both managed flags are on,
7995
+ # pull related prior verdicts from the Claude Managed Agents store and
7996
+ # append them AFTER local results. 5s hard timeout so a slow remote never
7997
+ # blocks the loop. On timeout or error, emit a fallback event and continue.
7998
+ if [ "$LOKI_MANAGED_AGENTS" = "true" ] && [ "$LOKI_MANAGED_MEMORY" = "true" ]; then
7999
+ local managed_start_ms
8000
+ managed_start_ms=$(python3 -c "import time; print(int(time.time()*1000))" 2>/dev/null || echo "0")
8001
+ local managed_out
8002
+ managed_out=$(
8003
+ cd "$PROJECT_DIR" 2>/dev/null && \
8004
+ LOKI_TARGET_DIR="$target_dir" \
8005
+ timeout 5 python3 -m memory.managed_memory.retrieve \
8006
+ --query "$goal" --top-k 3 2>/dev/null || true
8007
+ )
8008
+ if [ -n "$managed_out" ]; then
8009
+ echo ""
8010
+ echo "RELATED PRIOR LEARNINGS (managed store):"
8011
+ echo "$managed_out"
8012
+ else
8013
+ # No output could mean: flags off (unreachable here), timeout, or
8014
+ # zero hits. Emit a fallback event only if a timeout likely occurred.
8015
+ LOKI_TARGET_DIR="$target_dir" \
8016
+ python3 -c "from memory.managed_memory.events import emit_managed_event; emit_managed_event('managed_memory_retrieve_empty', {'phase': '$phase'})" 2>/dev/null || true
8017
+ fi
8018
+ fi
7980
8019
  }
7981
8020
 
7982
8021
  # Store episode trace after task completion
@@ -8080,14 +8119,21 @@ auto_capture_episode() {
8080
8119
  fi
8081
8120
 
8082
8121
  # Pass all context via environment variables (prevents injection)
8122
+ # v6.83.0: also stash the resolved episode path so the bash caller can
8123
+ # optionally shadow-write it to the managed store if importance >= 0.6.
8124
+ local episode_path_file="/tmp/loki-episode-path-$$"
8125
+ : > "$episode_path_file"
8083
8126
  _LOKI_PROJECT_DIR="$PROJECT_DIR" _LOKI_TARGET_DIR="$target_dir" \
8084
8127
  _LOKI_ITERATION="$iteration" _LOKI_EXIT_CODE="$exit_code" \
8085
8128
  _LOKI_RARV_PHASE="$rarv_phase" _LOKI_GOAL="$goal" \
8086
8129
  _LOKI_DURATION="$duration" _LOKI_OUTCOME="$outcome" \
8087
8130
  _LOKI_FILES_MODIFIED="$files_modified" _LOKI_GIT_COMMIT="$git_commit" \
8131
+ _LOKI_EPISODE_PATH_FILE="$episode_path_file" \
8088
8132
  python3 << 'PYEOF' 2>/dev/null || true
8089
8133
  import sys
8090
8134
  import os
8135
+ import json
8136
+ from pathlib import Path
8091
8137
 
8092
8138
  project_dir = os.environ.get('_LOKI_PROJECT_DIR', '')
8093
8139
  target_dir = os.environ.get('_LOKI_TARGET_DIR', '.')
@@ -8098,6 +8144,7 @@ duration = os.environ.get('_LOKI_DURATION', '0')
8098
8144
  outcome = os.environ.get('_LOKI_OUTCOME', 'success')
8099
8145
  files_modified = os.environ.get('_LOKI_FILES_MODIFIED', '')
8100
8146
  git_commit = os.environ.get('_LOKI_GIT_COMMIT', '')
8147
+ path_out_file = os.environ.get('_LOKI_EPISODE_PATH_FILE', '')
8101
8148
 
8102
8149
  sys.path.insert(0, project_dir)
8103
8150
  try:
@@ -8120,9 +8167,47 @@ try:
8120
8167
  trace.files_modified = [f for f in files_modified.split('|') if f] if files_modified else []
8121
8168
 
8122
8169
  engine.store_episode(trace)
8170
+
8171
+ # v6.83.0: surface the on-disk episode path + importance so bash can
8172
+ # decide whether to shadow-write. Writing to a known file (not stdout)
8173
+ # keeps the existing stdout contract intact.
8174
+ try:
8175
+ importance = float(getattr(trace, 'importance', 0.0) or 0.0)
8176
+ except (TypeError, ValueError):
8177
+ importance = 0.0
8178
+ episode_file = Path(f'{target_dir}/.loki/memory/episodic') / f'{trace.id}.json'
8179
+ if path_out_file:
8180
+ try:
8181
+ with open(path_out_file, 'w', encoding='utf-8') as f:
8182
+ json.dump({'path': str(episode_file), 'importance': importance}, f)
8183
+ except OSError:
8184
+ pass
8123
8185
  except Exception:
8124
8186
  pass # Silently fail -- memory capture must never break the loop
8125
8187
  PYEOF
8188
+
8189
+ # v6.83.0 Phase 1: RARV-C REFLECT/VERIFY shadow-write. Only when both
8190
+ # managed flags are on AND the episode meets the consolidation importance
8191
+ # threshold (>= 0.6). Fully non-blocking (backgrounded subprocess).
8192
+ if [ "$LOKI_MANAGED_AGENTS" = "true" ] && [ "$LOKI_MANAGED_MEMORY" = "true" ] \
8193
+ && [ -s "$episode_path_file" ]; then
8194
+ local _ep_path _ep_imp
8195
+ _ep_path=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d.get('path',''))" "$episode_path_file" 2>/dev/null || echo "")
8196
+ _ep_imp=$(python3 -c "import json,sys; d=json.load(open(sys.argv[1])); print(d.get('importance',0.0))" "$episode_path_file" 2>/dev/null || echo "0")
8197
+ if [ -n "$_ep_path" ] && [ -f "$_ep_path" ]; then
8198
+ local _above_threshold
8199
+ _above_threshold=$(python3 -c "print('yes' if float('$_ep_imp') >= 0.6 else 'no')" 2>/dev/null || echo "no")
8200
+ if [ "$_above_threshold" = "yes" ]; then
8201
+ (
8202
+ cd "$PROJECT_DIR" 2>/dev/null && \
8203
+ LOKI_TARGET_DIR="$target_dir" \
8204
+ timeout 15 python3 -m memory.managed_memory.shadow_write --path "$_ep_path" >/dev/null 2>&1 || true
8205
+ ) &
8206
+ disown 2>/dev/null || true
8207
+ fi
8208
+ fi
8209
+ fi
8210
+ rm -f "$episode_path_file" 2>/dev/null || true
8126
8211
  }
8127
8212
 
8128
8213
  # Run memory consolidation pipeline
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "6.82.0"
10
+ __version__ = "6.83.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v6.82.0
5
+ **Version:** v6.83.0
6
6
 
7
7
  ---
8
8
 
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '6.82.0'
60
+ __version__ = '6.83.0'
@@ -1,2 +1,3 @@
1
1
  mcp>=1.0.0
2
2
  chromadb>=1.0.0
3
+ anthropic>=0.40
@@ -0,0 +1,113 @@
1
+ """
2
+ Loki Managed Agents Memory package (v6.83.0 Phase 1).
3
+
4
+ Opt-in integration with Claude Managed Agents memory stores. All behavior in
5
+ this package is gated on the two environment variables:
6
+
7
+ LOKI_MANAGED_AGENTS parent switch (default: false)
8
+ LOKI_MANAGED_MEMORY child switch (default: false)
9
+
10
+ Both must be "true" for any API call to be issued. If the child is "true" while
11
+ the parent is "false", the loki runner fails fast at startup (see
12
+ autonomy/run.sh). If the flags are off, every exported function is a cheap
13
+ no-op -- importing this package will NOT trigger any network or SDK import
14
+ side-effects.
15
+
16
+ This package is intentionally the ONLY place in the codebase that imports the
17
+ `anthropic` SDK. A CI test (tests/managed_memory/test_sdk_isolation.sh)
18
+ enforces that invariant.
19
+
20
+ Exports:
21
+ is_enabled() - bool, True iff both flags are on
22
+ ManagedDisabled - raised when callers try to force an op while off
23
+ shadow_write_verdict(path) - shadow-write a council verdict JSON to the store
24
+ shadow_write_pattern(obj) - shadow-write a semantic pattern dict
25
+ retrieve_related_verdicts(q, top_k=3, store_id=None)
26
+ - return list of related prior verdicts
27
+ hydrate_patterns(mtime_floor)
28
+ - pull recent patterns and merge locally
29
+ probe_beta_header() - return the active beta header string
30
+ emit_managed_event(type, payload)
31
+ - low-level event writer
32
+
33
+ None of these functions raise on SDK or network errors. They return empty /
34
+ None and log one WARN line. Real SDK errors surface through
35
+ `.loki/managed/events.ndjson`.
36
+ """
37
+
38
+ from __future__ import annotations
39
+
40
+ import os
41
+ from typing import Optional
42
+
43
+ from ._beta import BETA_HEADER
44
+ from .events import emit_managed_event
45
+
46
+
47
+ class ManagedDisabled(Exception):
48
+ """Raised when a managed-memory operation is attempted while flags are off."""
49
+
50
+
51
+ def is_enabled() -> bool:
52
+ """True iff LOKI_MANAGED_AGENTS=true AND LOKI_MANAGED_MEMORY=true."""
53
+ parent = os.environ.get("LOKI_MANAGED_AGENTS", "").strip().lower() == "true"
54
+ child = os.environ.get("LOKI_MANAGED_MEMORY", "").strip().lower() == "true"
55
+ return parent and child
56
+
57
+
58
+ def probe_beta_header() -> str:
59
+ """Return the pinned managed-agents beta header."""
60
+ return BETA_HEADER
61
+
62
+
63
+ # Lazy re-exports. Importing the top-level package MUST NOT import the
64
+ # anthropic SDK, so we defer the real imports until the functions are called.
65
+
66
+
67
+ def shadow_write_verdict(verdict_json_path: str) -> None:
68
+ """Shadow-write a council verdict file to the managed store (opt-in)."""
69
+ if not is_enabled():
70
+ return None
71
+ from . import shadow_write as _sw # local import: gated on flags
72
+ return _sw.shadow_write_verdict(verdict_json_path)
73
+
74
+
75
+ def shadow_write_pattern(pattern: dict) -> None:
76
+ """Shadow-write a semantic pattern dict to the managed store (opt-in)."""
77
+ if not is_enabled():
78
+ return None
79
+ from . import shadow_write as _sw
80
+ return _sw.shadow_write_pattern(pattern)
81
+
82
+
83
+ def retrieve_related_verdicts(
84
+ query: str,
85
+ top_k: int = 3,
86
+ store_id: Optional[str] = None,
87
+ ):
88
+ """Return related verdicts from the managed store; [] when disabled or on error."""
89
+ if not is_enabled():
90
+ return []
91
+ from . import retrieve as _r
92
+ return _r.retrieve_related_verdicts(query, top_k=top_k, store_id=store_id)
93
+
94
+
95
+ def hydrate_patterns(local_mtime_floor: float):
96
+ """Pull semantic patterns updated after floor; no-op when disabled."""
97
+ if not is_enabled():
98
+ return None
99
+ from . import retrieve as _r
100
+ return _r.hydrate_patterns(local_mtime_floor)
101
+
102
+
103
+ __all__ = [
104
+ "BETA_HEADER",
105
+ "ManagedDisabled",
106
+ "emit_managed_event",
107
+ "hydrate_patterns",
108
+ "is_enabled",
109
+ "probe_beta_header",
110
+ "retrieve_related_verdicts",
111
+ "shadow_write_pattern",
112
+ "shadow_write_verdict",
113
+ ]
@@ -0,0 +1,11 @@
1
+ """
2
+ Loki Managed Agents Memory - Beta Header (v6.83.0 Phase 1).
3
+
4
+ Single source of truth for the anthropic-beta header required by Claude
5
+ Managed Agents. All callers in memory/managed_memory/ import BETA_HEADER
6
+ from here. Update this constant to roll to a new beta.
7
+ """
8
+
9
+ # Pin to the public Managed Agents beta channel current as of v6.83.0.
10
+ # This value is sent as the `anthropic-beta` HTTP header on every request.
11
+ BETA_HEADER = "managed-agents-2026-04-01"
@@ -0,0 +1,210 @@
1
+ """
2
+ Loki Managed Agents Memory - Client wrapper (v6.83.0 Phase 1).
3
+
4
+ Thin wrapper around the `anthropic` SDK. This is the ONLY file in the codebase
5
+ that imports `anthropic`. A CI invariant test enforces that.
6
+
7
+ The wrapper:
8
+ - Sets anthropic-beta: managed-agents-2026-04-01 on every request.
9
+ - Reads ANTHROPIC_API_KEY from env. Absence raises ManagedDisabled.
10
+ - Wraps every SDK call in a 10s hard timeout. Timeouts are treated as
11
+ recoverable: the caller decides whether to fall back.
12
+ - Never retries inside the client (no retry-storm). Callers implement
13
+ bounded retry (e.g. 409 precondition merge-and-retry-once).
14
+
15
+ NOTE on API surface: the exact Managed Agents memory endpoints are under a
16
+ beta channel. This wrapper implements a minimal, forward-compatible subset --
17
+ stores_list, stores_get_or_create, memory_create, memory_read, memories_list.
18
+ If the SDK version installed does not expose `beta.memory`, calls raise an
19
+ AttributeError which the callers translate into a ManagedDisabled/fallback.
20
+
21
+ Not tested end-to-end against a live ANTHROPIC_API_KEY in CI. Automated tests
22
+ use memory/managed_memory/fakes.py.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import hashlib
28
+ import os
29
+ import threading
30
+ from typing import Any, Dict, List, Optional
31
+
32
+ from . import ManagedDisabled
33
+ from ._beta import BETA_HEADER
34
+
35
+ _DEFAULT_TIMEOUT = 10.0 # seconds
36
+
37
+
38
+ def _check_flags_or_raise() -> None:
39
+ parent = os.environ.get("LOKI_MANAGED_AGENTS", "").strip().lower() == "true"
40
+ child = os.environ.get("LOKI_MANAGED_MEMORY", "").strip().lower() == "true"
41
+ if not (parent and child):
42
+ raise ManagedDisabled(
43
+ "managed memory flags are off "
44
+ "(LOKI_MANAGED_AGENTS and LOKI_MANAGED_MEMORY must both be 'true')"
45
+ )
46
+
47
+
48
+ def _require_api_key() -> str:
49
+ key = os.environ.get("ANTHROPIC_API_KEY", "").strip()
50
+ if not key:
51
+ raise ManagedDisabled("ANTHROPIC_API_KEY is not set")
52
+ return key
53
+
54
+
55
+ def compute_sha256(content: str) -> str:
56
+ """Stable content hash used as an optimistic precondition on writes."""
57
+ return hashlib.sha256(content.encode("utf-8")).hexdigest()
58
+
59
+
60
+ class ManagedClient:
61
+ """
62
+ Thin SDK wrapper. Instantiating this class imports anthropic and validates
63
+ credentials; callers should construct it lazily inside flag-gated paths.
64
+ """
65
+
66
+ def __init__(self, timeout: float = _DEFAULT_TIMEOUT) -> None:
67
+ _check_flags_or_raise()
68
+ api_key = _require_api_key()
69
+ # Import lazily so the top-level package stays SDK-free.
70
+ try:
71
+ import anthropic # noqa: F401 (imported for side-effects + symbol)
72
+ except ImportError as e: # pragma: no cover - import surface
73
+ raise ManagedDisabled(f"anthropic SDK not installed: {e}")
74
+
75
+ self._anthropic = anthropic
76
+ self._client = anthropic.Anthropic(
77
+ api_key=api_key,
78
+ timeout=timeout,
79
+ default_headers={"anthropic-beta": BETA_HEADER},
80
+ )
81
+ self._timeout = timeout
82
+
83
+ # ---------- helpers -------------------------------------------------
84
+
85
+ def _beta(self):
86
+ """Return the beta namespace, if the SDK exposes it.
87
+
88
+ Newer SDK versions expose `client.beta.memory.*`. If the attribute
89
+ path is missing we raise ManagedDisabled so callers can fall back.
90
+ """
91
+ beta = getattr(self._client, "beta", None)
92
+ if beta is None:
93
+ raise ManagedDisabled("anthropic SDK missing `beta` namespace")
94
+ return beta
95
+
96
+ # ---------- stores --------------------------------------------------
97
+
98
+ def stores_list(self) -> List[Dict[str, Any]]:
99
+ """List managed memory stores on this account (may be empty)."""
100
+ beta = self._beta()
101
+ stores = getattr(beta, "memory_stores", None) or getattr(beta, "stores", None)
102
+ if stores is None or not hasattr(stores, "list"):
103
+ raise ManagedDisabled("memory_stores API not available in SDK")
104
+ result = stores.list()
105
+ # SDK returns a pydantic model; normalize to list of dicts.
106
+ data = getattr(result, "data", result)
107
+ return [self._to_dict(x) for x in (data or [])]
108
+
109
+ def stores_get_or_create(
110
+ self, name: str, description: str = "", scope: str = "project"
111
+ ) -> Dict[str, Any]:
112
+ """Return existing store with `name` or create it."""
113
+ existing = [s for s in self.stores_list() if s.get("name") == name]
114
+ if existing:
115
+ return existing[0]
116
+ beta = self._beta()
117
+ stores = getattr(beta, "memory_stores", None) or getattr(beta, "stores", None)
118
+ if stores is None or not hasattr(stores, "create"):
119
+ raise ManagedDisabled("memory_stores.create not available in SDK")
120
+ created = stores.create(name=name, description=description, scope=scope)
121
+ return self._to_dict(created)
122
+
123
+ # ---------- memories ------------------------------------------------
124
+
125
+ def memory_create(
126
+ self,
127
+ store_id: str,
128
+ path: str,
129
+ content: str,
130
+ sha256_precondition: Optional[str] = None,
131
+ ) -> Dict[str, Any]:
132
+ """
133
+ Create a memory entry at `path` in `store_id`.
134
+
135
+ When sha256_precondition is supplied, this is an optimistic
136
+ concurrency hint: if the store already holds a different hash the
137
+ SDK is expected to surface a 409-shaped error. Callers handle the
138
+ 409 by re-reading, merging, and retrying once.
139
+ """
140
+ beta = self._beta()
141
+ memories = getattr(beta, "memories", None)
142
+ if memories is None or not hasattr(memories, "create"):
143
+ raise ManagedDisabled("memories.create not available in SDK")
144
+ kwargs: Dict[str, Any] = {
145
+ "store_id": store_id,
146
+ "path": path,
147
+ "content": content,
148
+ }
149
+ if sha256_precondition:
150
+ kwargs["if_match_sha256"] = sha256_precondition
151
+ created = memories.create(**kwargs)
152
+ return self._to_dict(created)
153
+
154
+ def memory_read(self, store_id: str, memory_id: str) -> Dict[str, Any]:
155
+ beta = self._beta()
156
+ memories = getattr(beta, "memories", None)
157
+ if memories is None or not hasattr(memories, "retrieve"):
158
+ raise ManagedDisabled("memories.retrieve not available in SDK")
159
+ got = memories.retrieve(store_id=store_id, memory_id=memory_id)
160
+ return self._to_dict(got)
161
+
162
+ def memories_list(
163
+ self, store_id: str, path_prefix: Optional[str] = None
164
+ ) -> List[Dict[str, Any]]:
165
+ beta = self._beta()
166
+ memories = getattr(beta, "memories", None)
167
+ if memories is None or not hasattr(memories, "list"):
168
+ raise ManagedDisabled("memories.list not available in SDK")
169
+ kwargs: Dict[str, Any] = {"store_id": store_id}
170
+ if path_prefix:
171
+ kwargs["path_prefix"] = path_prefix
172
+ result = memories.list(**kwargs)
173
+ data = getattr(result, "data", result)
174
+ return [self._to_dict(x) for x in (data or [])]
175
+
176
+ # ---------- internal ------------------------------------------------
177
+
178
+ @staticmethod
179
+ def _to_dict(obj: Any) -> Dict[str, Any]:
180
+ """Best-effort pydantic-or-dict to dict conversion."""
181
+ if isinstance(obj, dict):
182
+ return obj
183
+ to_dict = getattr(obj, "model_dump", None) or getattr(obj, "dict", None)
184
+ if callable(to_dict):
185
+ try:
186
+ return to_dict()
187
+ except TypeError:
188
+ return to_dict()
189
+ return {"raw": str(obj)}
190
+
191
+
192
+ # Optional helper for callers that want a thread-safe singleton.
193
+ _singleton: Optional[ManagedClient] = None
194
+ _singleton_lock = threading.Lock()
195
+
196
+
197
+ def get_client() -> ManagedClient:
198
+ """Return a lazily-constructed singleton. Raises ManagedDisabled if off."""
199
+ global _singleton
200
+ with _singleton_lock:
201
+ if _singleton is None:
202
+ _singleton = ManagedClient()
203
+ return _singleton
204
+
205
+
206
+ def reset_client() -> None:
207
+ """Test hook: drop the cached singleton so tests can swap implementations."""
208
+ global _singleton
209
+ with _singleton_lock:
210
+ _singleton = None
@@ -0,0 +1,79 @@
1
+ """
2
+ Loki Managed Agents Memory - Event emission (v6.83.0 Phase 1).
3
+
4
+ Appends structured JSONL events to .loki/managed/events.ndjson. Single-writer
5
+ convention: only code in memory/managed_memory/ writes to this file. Rotates
6
+ when the file exceeds 10MB.
7
+
8
+ Events are used to record fallbacks, shadow-write successes/failures, and
9
+ retrieve hits. The file is safe to tail for observability during development.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ from datetime import datetime, timezone
17
+ from pathlib import Path
18
+ from typing import Any, Dict, Optional
19
+
20
+ # 10 MB rotation threshold. Keeping rotation simple: rename to .<YYYYMMDD>.
21
+ _ROTATE_BYTES = 10 * 1024 * 1024
22
+
23
+
24
+ def _events_dir(target_dir: Optional[str] = None) -> Path:
25
+ base = target_dir or os.environ.get("LOKI_TARGET_DIR") or os.getcwd()
26
+ return Path(base) / ".loki" / "managed"
27
+
28
+
29
+ def _maybe_rotate(path: Path) -> None:
30
+ """Rotate the events file if it has exceeded the size threshold."""
31
+ try:
32
+ if path.exists() and path.stat().st_size >= _ROTATE_BYTES:
33
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
34
+ rotated = path.with_suffix(path.suffix + f".{stamp}")
35
+ # Best-effort rename; if another writer got there first, move on.
36
+ try:
37
+ path.rename(rotated)
38
+ except OSError:
39
+ pass
40
+ except OSError:
41
+ # If stat fails, skip rotation; next write will retry.
42
+ pass
43
+
44
+
45
+ def emit_managed_event(
46
+ event_type: str,
47
+ payload: Dict[str, Any],
48
+ target_dir: Optional[str] = None,
49
+ ) -> None:
50
+ """
51
+ Append a managed-memory event to .loki/managed/events.ndjson.
52
+
53
+ Never raises: on any I/O error the function silently returns. Callers
54
+ rely on this to keep the main RARV-C loop unblocked.
55
+
56
+ Args:
57
+ event_type: short tag, e.g. "managed_agents_fallback",
58
+ "managed_memory_retrieve", "managed_memory_shadow_write".
59
+ payload: JSON-serializable context for the event.
60
+ target_dir: optional project root override. Defaults to
61
+ LOKI_TARGET_DIR env or cwd.
62
+ """
63
+ try:
64
+ dir_path = _events_dir(target_dir)
65
+ dir_path.mkdir(parents=True, exist_ok=True)
66
+ path = dir_path / "events.ndjson"
67
+ _maybe_rotate(path)
68
+
69
+ record = {
70
+ "ts": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
71
+ "type": event_type,
72
+ "payload": payload,
73
+ }
74
+ # Line-buffered append; JSONL.
75
+ with open(path, "a", encoding="utf-8") as f:
76
+ f.write(json.dumps(record, default=str) + "\n")
77
+ except Exception:
78
+ # Never raise from the event emitter.
79
+ return