@meridiona/meridian-darwin-arm64 1.54.1 → 1.56.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/VERSION +1 -1
- package/bin/meridian +0 -0
- package/package.json +1 -1
- package/scripts/install-openobserve-daemon.sh +7 -3
- package/services/agents/observability.py +243 -21
- package/services/agents/pm_worklog_update/agents.py +11 -3
- package/services/agents/run_task_linker_mlx.py +342 -83
- package/services/agents/server.py +306 -84
- package/services/observability/dashboards/classifier-debug.json +174 -0
- package/services/observability/dashboards/pm-worklog-debug.json +140 -0
- package/services/pyproject.toml +1 -1
- package/ui.tar.gz +0 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.56.0
|
package/bin/meridian
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meridiona/meridian-darwin-arm64",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.56.0",
|
|
4
4
|
"description": "Prebuilt Meridian app for macOS arm64 (daemon binary + dashboard + Python services). Installed via @meridiona/meridian.",
|
|
5
5
|
"homepage": "https://github.com/Meridiona/meridian",
|
|
6
6
|
"repository": {
|
|
@@ -41,15 +41,19 @@ elif command -v openobserve >/dev/null 2>&1; then
|
|
|
41
41
|
fi
|
|
42
42
|
|
|
43
43
|
if [[ -z "${OO_BIN}" ]]; then
|
|
44
|
-
echo "→ OpenObserve binary not found — downloading v0.
|
|
44
|
+
echo "→ OpenObserve binary not found — downloading v0.90.3..."
|
|
45
45
|
_oo_arch="$(uname -m)"
|
|
46
46
|
case "$_oo_arch" in
|
|
47
47
|
arm64) _oo_arch="arm64" ;;
|
|
48
48
|
x86_64) _oo_arch="amd64" ;;
|
|
49
49
|
*) echo "✗ Unsupported arch: $_oo_arch" >&2; exit 1 ;;
|
|
50
50
|
esac
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
# GitHub release assets were removed for recent versions; binaries now live on
|
|
52
|
+
# the official downloads host. Trace deep-linking (dashboard drilldown into a
|
|
53
|
+
# single trace's spans) needs a modern build, so we pin a current stable.
|
|
54
|
+
# KEEP IN SYNC: the same version is pinned in install.sh — bump both together.
|
|
55
|
+
_oo_ver="v0.90.3"
|
|
56
|
+
_oo_url="https://downloads.openobserve.ai/releases/openobserve/${_oo_ver}/openobserve-${_oo_ver}-darwin-${_oo_arch}.tar.gz"
|
|
53
57
|
mkdir -p "${HOME}/.openobserve"
|
|
54
58
|
if curl -fsSL -o "${HOME}/.openobserve/openobserve.tar.gz" "$_oo_url" \
|
|
55
59
|
&& tar -xzf "${HOME}/.openobserve/openobserve.tar.gz" -C "${HOME}/.openobserve" \
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
A single `setup(agent_name)` call wires up:
|
|
4
4
|
|
|
5
5
|
* an OTel `TracerProvider` with `service.name=agent_name`
|
|
6
|
-
* a `BatchSpanProcessor`
|
|
7
|
-
(
|
|
6
|
+
* a `BatchSpanProcessor` writing OTLP/HTTP-protobuf spans to the durable
|
|
7
|
+
telemetry spool (the Rust daemon's shipper drains it to OpenObserve)
|
|
8
|
+
* a `LoggerProvider` + spool log handler so every `logging.LogRecord` is also
|
|
9
|
+
spooled (correlated to the active span), mirroring the Rust daemon's
|
|
10
|
+
`OpenTelemetryTracingBridge`
|
|
8
11
|
* W3C `TraceContextTextMapPropagator` as the global propagator so each
|
|
9
12
|
agent can pick up the Rust daemon's `traceparent` and continue the trace
|
|
10
13
|
* `LoggingInstrumentor` so every `logging.LogRecord` carries
|
|
@@ -13,6 +16,12 @@ A single `setup(agent_name)` call wires up:
|
|
|
13
16
|
under `~/.meridian/logs/{agent_name}.jsonl` plus stderr — both ingestable
|
|
14
17
|
by OpenObserve's log pipeline without further parsing.
|
|
15
18
|
|
|
19
|
+
Export is gated by the SAME `~/.meridian/settings.json` the Rust daemon reads —
|
|
20
|
+
the `otlp_enabled` toggle — so the dashboard Settings page is the single source
|
|
21
|
+
of truth for both processes. Delivery (endpoint + Basic-auth credentials) is
|
|
22
|
+
owned entirely by the Rust shipper; Python only ever writes to the spool. The
|
|
23
|
+
legacy `MERIDIAN_OO_AUTH` env credential is deprecated and ignored.
|
|
24
|
+
|
|
16
25
|
`extract_parent_context(traceparent)` is the helper agents use to continue
|
|
17
26
|
a span emitted by another process — typically the Rust ETL or another
|
|
18
27
|
agent stage.
|
|
@@ -20,42 +29,217 @@ agent stage.
|
|
|
20
29
|
Idempotent: calling `setup` twice is a no-op for the second call (returns
|
|
21
30
|
the existing tracer). This matters because both the daemon and the
|
|
22
31
|
single-shot CLI paths funnel through the same module.
|
|
32
|
+
|
|
33
|
+
Spool durability: when `otlp_enabled` is true (regardless of whether
|
|
34
|
+
credentials are present), spans and logs are written atomically to
|
|
35
|
+
`~/.meridian/telemetry/pending/<signal>-<unix_micros>-<seq>.otlp` via
|
|
36
|
+
`SpoolSpanExporter` and `SpoolLogExporter`. The Rust daemon's shipper
|
|
37
|
+
task drains that shared directory to OpenObserve — Python does NOT need
|
|
38
|
+
its own shipper, and credential resolution lives there, not here.
|
|
23
39
|
"""
|
|
24
40
|
from __future__ import annotations
|
|
25
41
|
|
|
42
|
+
import json
|
|
26
43
|
import logging
|
|
27
44
|
import logging.handlers
|
|
28
45
|
import os
|
|
29
46
|
import sys
|
|
47
|
+
import threading
|
|
48
|
+
import time
|
|
30
49
|
from pathlib import Path
|
|
31
50
|
from typing import Optional
|
|
32
51
|
|
|
33
52
|
from opentelemetry import trace
|
|
53
|
+
from opentelemetry._logs import set_logger_provider
|
|
34
54
|
from opentelemetry.context import Context
|
|
35
|
-
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
36
|
-
OTLPSpanExporter,
|
|
37
|
-
)
|
|
38
55
|
from opentelemetry.instrumentation.logging import LoggingInstrumentor
|
|
39
56
|
from opentelemetry.propagate import set_global_textmap
|
|
57
|
+
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
58
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExportResult
|
|
40
59
|
from opentelemetry.sdk.resources import Resource
|
|
41
60
|
from opentelemetry.sdk.trace import TracerProvider
|
|
42
|
-
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
61
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExportResult
|
|
43
62
|
from opentelemetry.trace.propagation.tracecontext import (
|
|
44
63
|
TraceContextTextMapPropagator,
|
|
45
64
|
)
|
|
46
65
|
from pythonjsonlogger import jsonlogger
|
|
47
66
|
|
|
48
67
|
|
|
68
|
+
# ──────────────────────── Spool exporters ──────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
_spool_seq_lock = threading.Lock()
|
|
71
|
+
_spool_seq = 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _next_spool_seq() -> int:
|
|
75
|
+
global _spool_seq
|
|
76
|
+
with _spool_seq_lock:
|
|
77
|
+
val = _spool_seq
|
|
78
|
+
_spool_seq += 1
|
|
79
|
+
return val
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _resolve_telemetry_dir() -> Path:
|
|
83
|
+
"""Mirror of the Rust writer's resolve_telemetry_dir().
|
|
84
|
+
|
|
85
|
+
Precedence: MERIDIAN_TELEMETRY_DIR env → ~/.meridian/telemetry.
|
|
86
|
+
"""
|
|
87
|
+
env = os.environ.get("MERIDIAN_TELEMETRY_DIR", "").strip()
|
|
88
|
+
if env:
|
|
89
|
+
return Path(env).expanduser()
|
|
90
|
+
home = Path.home()
|
|
91
|
+
return home / ".meridian" / "telemetry"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _write_spool(signal: str, payload: bytes) -> None:
|
|
95
|
+
"""Atomically write payload to ~/.meridian/telemetry/pending/.
|
|
96
|
+
|
|
97
|
+
Filename: <signal>-<unix_micros>-<seq>.otlp
|
|
98
|
+
Write via <name>.tmp then rename so the Rust shipper never sees partial files.
|
|
99
|
+
"""
|
|
100
|
+
base = _resolve_telemetry_dir()
|
|
101
|
+
pending = base / "pending"
|
|
102
|
+
pending.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
|
|
104
|
+
micros = int(time.time() * 1_000_000)
|
|
105
|
+
seq = _next_spool_seq()
|
|
106
|
+
filename = f"{signal}-{micros}-{seq}.otlp"
|
|
107
|
+
final_path = pending / filename
|
|
108
|
+
tmp_path = pending / f"{filename}.tmp"
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
# fsync the tmp file before the rename so a power loss can't leave a
|
|
112
|
+
# rename (metadata) durable while the data blocks are still in page
|
|
113
|
+
# cache — that would surface a truncated .otlp the shipper POSTs (→ a
|
|
114
|
+
# 400). Mirrors the Rust writer's sync_all() + dir fsync.
|
|
115
|
+
with open(tmp_path, "wb") as fh:
|
|
116
|
+
fh.write(payload)
|
|
117
|
+
fh.flush()
|
|
118
|
+
os.fsync(fh.fileno())
|
|
119
|
+
tmp_path.rename(final_path)
|
|
120
|
+
try:
|
|
121
|
+
dir_fd = os.open(str(pending), os.O_RDONLY)
|
|
122
|
+
try:
|
|
123
|
+
os.fsync(dir_fd)
|
|
124
|
+
finally:
|
|
125
|
+
os.close(dir_fd)
|
|
126
|
+
except OSError:
|
|
127
|
+
# Directory fsync is best-effort (not all FS/platforms support it);
|
|
128
|
+
# the tmp-file fsync already guarantees the data is on disk.
|
|
129
|
+
pass
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
logging.getLogger(__name__).warning(
|
|
132
|
+
"telemetry spool write failed — payload dropped",
|
|
133
|
+
extra={"signal": signal, "error": str(exc)},
|
|
134
|
+
)
|
|
135
|
+
# Best-effort cleanup so a failed write never strands a .tmp orphan
|
|
136
|
+
# (the Rust shipper sweeps these, but don't rely on it).
|
|
137
|
+
try:
|
|
138
|
+
tmp_path.unlink(missing_ok=True)
|
|
139
|
+
except OSError as cleanup_exc:
|
|
140
|
+
# Cleanup failure is non-fatal: write already failed and we avoid
|
|
141
|
+
# raising secondary errors from best-effort orphan removal.
|
|
142
|
+
logging.getLogger(__name__).debug(
|
|
143
|
+
"telemetry spool tmp cleanup failed",
|
|
144
|
+
extra={"tmp_path": str(tmp_path), "error": str(cleanup_exc)},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SpoolSpanExporter:
|
|
149
|
+
"""Span exporter that writes serialised OTLP payloads to the spool dir.
|
|
150
|
+
|
|
151
|
+
Wraps the SDK's encode_spans() to produce the same wire bytes the real
|
|
152
|
+
OTLPSpanExporter would POST. The Rust shipper drains pending/ to OO.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def export(self, spans): # type: ignore[override]
|
|
156
|
+
try:
|
|
157
|
+
from opentelemetry.exporter.otlp.proto.common.trace_encoder import (
|
|
158
|
+
encode_spans,
|
|
159
|
+
)
|
|
160
|
+
payload = encode_spans(spans).SerializeToString()
|
|
161
|
+
_write_spool("traces", payload)
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
logging.getLogger(__name__).warning(
|
|
164
|
+
"SpoolSpanExporter.export failed", extra={"error": str(exc)}
|
|
165
|
+
)
|
|
166
|
+
return SpanExportResult.SUCCESS
|
|
167
|
+
|
|
168
|
+
def shutdown(self) -> None:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
def force_flush(self, timeout_millis: int = 30_000) -> bool:
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class SpoolLogExporter:
|
|
176
|
+
"""Log exporter that writes serialised OTLP payloads to the spool dir.
|
|
177
|
+
|
|
178
|
+
Mirrors SpoolSpanExporter for the log signal.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
def export(self, log_data): # type: ignore[override]
|
|
182
|
+
try:
|
|
183
|
+
from opentelemetry.exporter.otlp.proto.common._log_encoder import (
|
|
184
|
+
encode_logs,
|
|
185
|
+
)
|
|
186
|
+
payload = encode_logs(log_data).SerializeToString()
|
|
187
|
+
_write_spool("logs", payload)
|
|
188
|
+
except Exception as exc:
|
|
189
|
+
logging.getLogger(__name__).warning(
|
|
190
|
+
"SpoolLogExporter.export failed", extra={"error": str(exc)}
|
|
191
|
+
)
|
|
192
|
+
return LogExportResult.SUCCESS
|
|
193
|
+
|
|
194
|
+
def shutdown(self) -> None:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
def force_flush(self, timeout_millis: int = 30_000) -> bool:
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
|
|
49
201
|
# ──────────────────────── Config ───────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
202
|
+
DEFAULT_LOG_DIR = Path.home() / ".meridian" / "logs"
|
|
203
|
+
# Single source of truth for the export TOGGLE — the SAME file the Rust daemon
|
|
204
|
+
# reads (see `src/observability.rs::resolve_otlp_target`). Delivery credentials
|
|
205
|
+
# live there too: the dashboard Settings page writes here and the Rust shipper
|
|
206
|
+
# picks them up. This process only reads `otlp_enabled` to decide whether to
|
|
207
|
+
# spool — it never delivers, so it needs no endpoint/credentials of its own.
|
|
208
|
+
_SETTINGS_PATH = Path(
|
|
209
|
+
os.environ.get("MERIDIAN_SETTINGS_PATH")
|
|
210
|
+
or (Path.home() / ".meridian" / "settings.json")
|
|
211
|
+
)
|
|
53
212
|
|
|
54
213
|
_NOISY_LOGGERS = ("urllib3", "httpx", "httpcore", "openai", "botocore")
|
|
55
214
|
|
|
56
215
|
# Track which agents have been configured so a second setup() call is a no-op.
|
|
57
216
|
_INITIALISED: dict[str, trace.Tracer] = {}
|
|
58
217
|
_PROCESS_SERVICE_NAME: str | None = None
|
|
218
|
+
# Held so shutdown() can flush log records the same way it flushes spans.
|
|
219
|
+
_LOGGER_PROVIDER: LoggerProvider | None = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _load_settings() -> dict[str, object]:
|
|
223
|
+
"""Read `~/.meridian/settings.json`; empty dict if absent/unreadable."""
|
|
224
|
+
try:
|
|
225
|
+
with _SETTINGS_PATH.open(encoding="utf-8") as fh:
|
|
226
|
+
data = json.load(fh)
|
|
227
|
+
return data if isinstance(data, dict) else {}
|
|
228
|
+
except (OSError, ValueError):
|
|
229
|
+
return {}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_otlp_enabled() -> bool:
|
|
233
|
+
"""Return True when the otlp_enabled toggle is on and tracing is not disabled.
|
|
234
|
+
|
|
235
|
+
Deliberately does NOT check credentials — used to gate the spool exporters
|
|
236
|
+
so telemetry is captured even when OO credentials are absent (the Rust
|
|
237
|
+
shipper delivers when they are provided later, and warns once if a user
|
|
238
|
+
leaves the toggle on with no credentials).
|
|
239
|
+
"""
|
|
240
|
+
if os.environ.get("MERIDIAN_TRACING_DISABLED", "").lower() in ("1", "true", "yes"):
|
|
241
|
+
return False
|
|
242
|
+
return bool(_load_settings().get("otlp_enabled"))
|
|
59
243
|
|
|
60
244
|
|
|
61
245
|
# ──────────────────────── Public API ───────────────────────────────────────────
|
|
@@ -105,6 +289,12 @@ def shutdown() -> None:
|
|
|
105
289
|
if hasattr(provider, "shutdown"):
|
|
106
290
|
provider.shutdown()
|
|
107
291
|
|
|
292
|
+
# Flush queued log records too — BatchLogRecordProcessor drops them on
|
|
293
|
+
# interpreter exit otherwise, the same hazard as spans.
|
|
294
|
+
if _LOGGER_PROVIDER is not None:
|
|
295
|
+
_LOGGER_PROVIDER.force_flush(timeout_millis=5_000)
|
|
296
|
+
_LOGGER_PROVIDER.shutdown()
|
|
297
|
+
|
|
108
298
|
|
|
109
299
|
def extract_parent_context(traceparent: Optional[str]) -> Optional[Context]:
|
|
110
300
|
"""Parse an incoming W3C `traceparent` header into an OTel `Context`.
|
|
@@ -123,18 +313,10 @@ def _configure_tracing(agent_name: str) -> None:
|
|
|
123
313
|
resource = Resource.create({"service.name": agent_name})
|
|
124
314
|
provider = TracerProvider(resource=resource)
|
|
125
315
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
)
|
|
131
|
-
if not disabled and endpoint:
|
|
132
|
-
headers: dict[str, str] = {}
|
|
133
|
-
auth = os.environ.get("MERIDIAN_OO_AUTH")
|
|
134
|
-
if auth:
|
|
135
|
-
headers["Authorization"] = f"Basic {auth}"
|
|
136
|
-
exporter = OTLPSpanExporter(endpoint=endpoint, headers=headers)
|
|
137
|
-
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
316
|
+
# Wire the spool exporter when otlp_enabled is true (even without creds).
|
|
317
|
+
# The Rust shipper drains the shared spool dir when a target is available.
|
|
318
|
+
if _is_otlp_enabled():
|
|
319
|
+
provider.add_span_processor(BatchSpanProcessor(SpoolSpanExporter()))
|
|
138
320
|
|
|
139
321
|
# Set as the global provider. OTel's `set_tracer_provider` warns if
|
|
140
322
|
# someone already configured a provider in-process; we accept that and
|
|
@@ -143,6 +325,30 @@ def _configure_tracing(agent_name: str) -> None:
|
|
|
143
325
|
set_global_textmap(TraceContextTextMapPropagator())
|
|
144
326
|
|
|
145
327
|
|
|
328
|
+
def _configure_log_export(agent_name: str) -> Optional[logging.Handler]:
|
|
329
|
+
"""Build a spool-logs handler so every `log.*` record reaches OpenObserve,
|
|
330
|
+
correlated to the active span by trace_id/span_id — the Python counterpart
|
|
331
|
+
of the Rust daemon's `OpenTelemetryTracingBridge`.
|
|
332
|
+
|
|
333
|
+
Returns the handler (caller attaches it to root) or `None` when export is
|
|
334
|
+
disabled, in which case logs still go to the JSONL file + stdout/stderr.
|
|
335
|
+
|
|
336
|
+
When `otlp_enabled` is true the spool log exporter is always wired (even
|
|
337
|
+
without credentials) — the Rust shipper handles delivery.
|
|
338
|
+
"""
|
|
339
|
+
global _LOGGER_PROVIDER
|
|
340
|
+
|
|
341
|
+
if not _is_otlp_enabled():
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
resource = Resource.create({"service.name": agent_name})
|
|
345
|
+
provider = LoggerProvider(resource=resource)
|
|
346
|
+
provider.add_log_record_processor(BatchLogRecordProcessor(SpoolLogExporter()))
|
|
347
|
+
set_logger_provider(provider)
|
|
348
|
+
_LOGGER_PROVIDER = provider
|
|
349
|
+
return LoggingHandler(level=logging.NOTSET, logger_provider=provider)
|
|
350
|
+
|
|
351
|
+
|
|
146
352
|
# ──────────────────────── Logging setup ────────────────────────────────────────
|
|
147
353
|
def _configure_logging(agent_name: str) -> None:
|
|
148
354
|
log_dir = Path(os.environ.get("MERIDIAN_LOG_DIR") or DEFAULT_LOG_DIR)
|
|
@@ -204,6 +410,22 @@ def _configure_logging(agent_name: str) -> None:
|
|
|
204
410
|
root.addHandler(file_h)
|
|
205
411
|
root.addHandler(stdout_h)
|
|
206
412
|
root.addHandler(stderr_h)
|
|
413
|
+
# Spool every record for OpenObserve via OTLP/HTTP logs too, when export is
|
|
414
|
+
# enabled. The OTel LoggingHandler reads the active span context, so each
|
|
415
|
+
# OO log row carries the trace_id/span_id that ties it to the classifier's
|
|
416
|
+
# span waterfall. No-op (None) when OTLP is disabled.
|
|
417
|
+
# The spool handler already carries service.name via the OTel Resource, so it
|
|
418
|
+
# needs no _ServiceFilter (that would duplicate the attribute on each record).
|
|
419
|
+
otlp_log_h = _configure_log_export(agent_name)
|
|
420
|
+
if otlp_log_h is not None:
|
|
421
|
+
# Do NOT feed the spool handler's OWN transport/encoder logs back into
|
|
422
|
+
# the spool: on a hiccup httpx/urllib3/opentelemetry emit WARNING+
|
|
423
|
+
# records which this root handler would try to spool → more failures (a
|
|
424
|
+
# log→export→log loop). Drop those from THIS handler only — they still
|
|
425
|
+
# reach the file/stderr handlers.
|
|
426
|
+
_otlp_excluded = ("httpx", "httpcore", "urllib3", "grpc", "opentelemetry")
|
|
427
|
+
otlp_log_h.addFilter(lambda r: not r.name.startswith(_otlp_excluded))
|
|
428
|
+
root.addHandler(otlp_log_h)
|
|
207
429
|
root.setLevel(level)
|
|
208
430
|
|
|
209
431
|
for noisy in _NOISY_LOGGERS:
|
|
@@ -62,7 +62,8 @@ def build_synth_agent(
|
|
|
62
62
|
debug_level: 1 or 2 (verbose).
|
|
63
63
|
"""
|
|
64
64
|
from agno.agent import Agent
|
|
65
|
-
|
|
65
|
+
# PII guardrail disabled for now — not needed.
|
|
66
|
+
# from agno.guardrails import PIIDetectionGuardrail
|
|
66
67
|
from agno.skills import LocalSkills, Skills
|
|
67
68
|
|
|
68
69
|
return Agent(
|
|
@@ -98,11 +99,18 @@ def build_synth_agent(
|
|
|
98
99
|
SessionBundleSizeGuard(max_tokens=80_000),
|
|
99
100
|
],
|
|
100
101
|
post_hooks=[
|
|
101
|
-
PIIDetectionGuardrail
|
|
102
|
+
# PIIDetectionGuardrail disabled — it's an input guardrail and was
|
|
103
|
+
# wrongly wired into post_hooks (post-hooks get run_output, not
|
|
104
|
+
# run_input → TypeError). Not needed for now.
|
|
102
105
|
time_spent_sanity_check,
|
|
103
106
|
],
|
|
104
107
|
output_schema=JiraUpdate,
|
|
105
|
-
use_json_mode=
|
|
108
|
+
# use_json_mode=False → agno sends the full JiraUpdate json_schema as the
|
|
109
|
+
# request's response_format (not a bare {"type":"json_object"}). The MLX
|
|
110
|
+
# /v1/chat/completions handler reads that schema and FSM-constrains
|
|
111
|
+
# decoding with outlines, so the reasoning model physically cannot leak
|
|
112
|
+
# chain-of-thought prose instead of the JSON object.
|
|
113
|
+
use_json_mode=False,
|
|
106
114
|
add_datetime_to_context=True,
|
|
107
115
|
add_history_to_context=False,
|
|
108
116
|
markdown=False,
|