@meridiona/meridian-darwin-arm64 1.67.0 → 1.68.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/bin/meridian-tray +0 -0
- package/package.json +1 -1
- package/scripts/install-openobserve-daemon.sh +30 -14
- package/services/agents/mlx_classifier.py +1 -1
- package/services/agents/observability.py +191 -61
- package/services/agents/prompts/activity_report.py +34 -53
- package/services/agents/routes/activity.py +120 -30
- package/services/agents/routes/distill.py +5 -4
- package/services/agents/routes/worklog.py +17 -14
- package/services/agents/server.py +8 -4
- package/services/agents/session_distiller.py +29 -6
- package/services/agents/time_utils.py +56 -0
- package/services/agents/worklog_pipeline/db.py +59 -14
- package/services/agents/worklog_pipeline/models.py +7 -3
- package/services/agents/worklog_pipeline/pipeline.py +148 -64
- package/services/agents/worklog_pipeline/prompts/propose_ticket.py +3 -1
- package/services/agents/worklog_pipeline/workflow.py +4 -1
- package/services/agents/worklog_pipeline/worklog.py +1 -0
- package/services/observability/dashboards/activity-summary.json +455 -0
- package/services/observability/dashboards/pm-worklog-hour.json +192 -0
- package/services/observability/dashboards/session-distiller.json +504 -0
- package/services/pyproject.toml +3 -1
- package/services/skills/coding-agent/session-summary/SKILL.md +13 -5
- package/services/uv.lock +417 -0
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.68.0
|
package/bin/meridian
CHANGED
|
Binary file
|
package/bin/meridian-tray
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.68.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": {
|
|
@@ -267,13 +267,15 @@ st, body = call("GET", f"/api/{org}/dashboards")
|
|
|
267
267
|
if st != 200:
|
|
268
268
|
print(f" ⚠ could not list dashboards ({st}) — skipping dashboard import")
|
|
269
269
|
sys.exit(0)
|
|
270
|
-
|
|
270
|
+
id_map = {} # title -> dashboard_id
|
|
271
271
|
for d in json.loads(body).get("dashboards", []):
|
|
272
|
-
|
|
272
|
+
did = d.get("dashboard_id", "")
|
|
273
|
+
for v in ("v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"):
|
|
273
274
|
if d.get(v) and d[v].get("title"):
|
|
274
|
-
|
|
275
|
+
id_map[d[v]["title"]] = did
|
|
276
|
+
break
|
|
275
277
|
|
|
276
|
-
created =
|
|
278
|
+
created = updated = failed = 0
|
|
277
279
|
for path in sorted(glob.glob(os.path.join(dash_dir, "*.json"))):
|
|
278
280
|
try:
|
|
279
281
|
dash = json.load(open(path))
|
|
@@ -282,18 +284,32 @@ for path in sorted(glob.glob(os.path.join(dash_dir, "*.json"))):
|
|
|
282
284
|
failed += 1
|
|
283
285
|
continue
|
|
284
286
|
title = dash.get("title", "")
|
|
285
|
-
if title in
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
287
|
+
if title in id_map:
|
|
288
|
+
# OO PUT requires an internal hash that can't be round-tripped reliably.
|
|
289
|
+
# Delete + re-create is the safe upsert path.
|
|
290
|
+
did = id_map[title]
|
|
291
|
+
st_d, _ = call("DELETE", f"/api/{org}/dashboards/{did}")
|
|
292
|
+
if st_d not in (200, 204):
|
|
293
|
+
failed += 1
|
|
294
|
+
print(f" ⚠ delete failed {title} ({st_d}) — skipping")
|
|
295
|
+
continue
|
|
296
|
+
st, resp = call("POST", f"/api/{org}/dashboards?folder=default", dash)
|
|
297
|
+
if st in (200, 201):
|
|
298
|
+
updated += 1
|
|
299
|
+
print(f" ✓ updated dashboard: {title}")
|
|
300
|
+
else:
|
|
301
|
+
failed += 1
|
|
302
|
+
print(f" ⚠ failed to re-create {os.path.basename(path)} ({st}): {resp[:200].decode(errors='replace')}")
|
|
292
303
|
else:
|
|
293
|
-
|
|
294
|
-
|
|
304
|
+
st, resp = call("POST", f"/api/{org}/dashboards?folder=default", dash)
|
|
305
|
+
if st in (200, 201):
|
|
306
|
+
created += 1
|
|
307
|
+
print(f" ✓ imported dashboard: {title}")
|
|
308
|
+
else:
|
|
309
|
+
failed += 1
|
|
310
|
+
print(f" ⚠ failed to import {os.path.basename(path)} ({st}): {resp[:200].decode(errors='replace')}")
|
|
295
311
|
|
|
296
|
-
print(f"→ dashboards: {created} imported, {
|
|
312
|
+
print(f"→ dashboards: {created} imported, {updated} updated, {failed} failed")
|
|
297
313
|
PYEOF
|
|
298
314
|
return 0
|
|
299
315
|
}
|
|
@@ -18,7 +18,7 @@ from typing import Any, Iterator
|
|
|
18
18
|
from agents import model_registry, observability
|
|
19
19
|
|
|
20
20
|
log = logging.getLogger("agents.mlx_classifier")
|
|
21
|
-
tracer = observability.setup("meridian-mlx-
|
|
21
|
+
tracer = observability.setup("meridian-mlx-server")
|
|
22
22
|
|
|
23
23
|
# Generative/classifier checkpoint — resolved from the model registry (the single
|
|
24
24
|
# source of truth for all three pipeline models), env-overridable via MERIDIAN_LLM_ID.
|
|
@@ -3,39 +3,30 @@
|
|
|
3
3
|
A single `setup(agent_name)` call wires up:
|
|
4
4
|
|
|
5
5
|
* an OTel `TracerProvider` with `service.name=agent_name`
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
* direct OTLP/HTTP export to OpenObserve when `otlp_enabled=true` and
|
|
7
|
+
credentials are set in `~/.meridian/settings.json` — no Rust daemon needed
|
|
8
|
+
* spool fallback (`~/.meridian/telemetry/pending/`) when toggle is on but
|
|
9
|
+
credentials are absent; the Rust daemon drains that directory to OO
|
|
10
|
+
* a `LoggerProvider` + matching log handler so every `logging.LogRecord`
|
|
11
|
+
is correlated to the active span via trace_id/span_id
|
|
11
12
|
* W3C `TraceContextTextMapPropagator` as the global propagator so each
|
|
12
13
|
agent can pick up the Rust daemon's `traceparent` and continue the trace
|
|
13
14
|
* `LoggingInstrumentor` so every `logging.LogRecord` carries
|
|
14
15
|
`otelTraceID` / `otelSpanID` attributes for OpenObserve correlation
|
|
15
16
|
* a JSON formatter (`python-json-logger`) writing daily-rotated JSONL files
|
|
16
|
-
under `~/.meridian/logs/{agent_name}.jsonl` plus stderr
|
|
17
|
-
by OpenObserve's log pipeline without further parsing.
|
|
17
|
+
under `~/.meridian/logs/{agent_name}.jsonl` plus stderr
|
|
18
18
|
|
|
19
|
-
Export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
legacy `MERIDIAN_OO_AUTH` env credential is deprecated and ignored.
|
|
19
|
+
Export gate: `~/.meridian/settings.json` `otlp_enabled` toggle + OO credentials
|
|
20
|
+
(`oo_email` / `oo_password`). When both are present the Python service ships
|
|
21
|
+
directly to OO — it does NOT depend on the Rust daemon for delivery. The spool
|
|
22
|
+
path is a fallback for when credentials are absent (daemon delivers later).
|
|
24
23
|
|
|
25
24
|
`extract_parent_context(traceparent)` is the helper agents use to continue
|
|
26
25
|
a span emitted by another process — typically the Rust ETL or another
|
|
27
26
|
agent stage.
|
|
28
27
|
|
|
29
28
|
Idempotent: calling `setup` twice is a no-op for the second call (returns
|
|
30
|
-
the existing tracer).
|
|
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.
|
|
29
|
+
the existing tracer).
|
|
39
30
|
"""
|
|
40
31
|
from __future__ import annotations
|
|
41
32
|
|
|
@@ -55,10 +46,10 @@ from opentelemetry.context import Context
|
|
|
55
46
|
from opentelemetry.instrumentation.logging import LoggingInstrumentor
|
|
56
47
|
from opentelemetry.propagate import set_global_textmap
|
|
57
48
|
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
|
58
|
-
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, LogExportResult
|
|
49
|
+
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor, SimpleLogRecordProcessor, LogExportResult
|
|
59
50
|
from opentelemetry.sdk.resources import Resource
|
|
60
51
|
from opentelemetry.sdk.trace import TracerProvider
|
|
61
|
-
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExportResult
|
|
52
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor, SpanExportResult
|
|
62
53
|
from opentelemetry.trace.propagation.tracecontext import (
|
|
63
54
|
TraceContextTextMapPropagator,
|
|
64
55
|
)
|
|
@@ -200,11 +191,8 @@ class SpoolLogExporter:
|
|
|
200
191
|
|
|
201
192
|
# ──────────────────────── Config ───────────────────────────────────────────────
|
|
202
193
|
DEFAULT_LOG_DIR = Path.home() / ".meridian" / "logs"
|
|
203
|
-
#
|
|
204
|
-
#
|
|
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.
|
|
194
|
+
# Same settings.json the Rust daemon reads. When otlp_enabled=true and
|
|
195
|
+
# oo_email/oo_password are set, Python ships directly to OO (no daemon needed).
|
|
208
196
|
_SETTINGS_PATH = Path(
|
|
209
197
|
os.environ.get("MERIDIAN_SETTINGS_PATH")
|
|
210
198
|
or (Path.home() / ".meridian" / "settings.json")
|
|
@@ -230,13 +218,7 @@ def _load_settings() -> dict[str, object]:
|
|
|
230
218
|
|
|
231
219
|
|
|
232
220
|
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
|
-
"""
|
|
221
|
+
"""Return True when the otlp_enabled toggle is on and tracing is not disabled."""
|
|
240
222
|
if os.environ.get("MERIDIAN_TRACING_DISABLED", "").lower() in ("1", "true", "yes"):
|
|
241
223
|
return False
|
|
242
224
|
return bool(_load_settings().get("otlp_enabled"))
|
|
@@ -280,22 +262,16 @@ def setup(agent_name: str) -> trace.Tracer:
|
|
|
280
262
|
|
|
281
263
|
|
|
282
264
|
def shutdown() -> None:
|
|
283
|
-
"""
|
|
265
|
+
"""Shut down the global TracerProvider and log provider.
|
|
284
266
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
explicitly flushed.
|
|
267
|
+
BatchSpanProcessor/BatchLogRecordProcessor queue spans asynchronously;
|
|
268
|
+
calling shutdown() flushes the queue before releasing resources.
|
|
288
269
|
"""
|
|
289
270
|
provider = trace.get_tracer_provider()
|
|
290
|
-
if hasattr(provider, "force_flush"):
|
|
291
|
-
provider.force_flush(timeout_millis=5_000)
|
|
292
271
|
if hasattr(provider, "shutdown"):
|
|
293
272
|
provider.shutdown()
|
|
294
273
|
|
|
295
|
-
# Flush queued log records too — BatchLogRecordProcessor drops them on
|
|
296
|
-
# interpreter exit otherwise, the same hazard as spans.
|
|
297
274
|
if _LOGGER_PROVIDER is not None:
|
|
298
|
-
_LOGGER_PROVIDER.force_flush(timeout_millis=5_000)
|
|
299
275
|
_LOGGER_PROVIDER.shutdown()
|
|
300
276
|
|
|
301
277
|
|
|
@@ -312,41 +288,86 @@ def extract_parent_context(traceparent: Optional[str]) -> Optional[Context]:
|
|
|
312
288
|
|
|
313
289
|
|
|
314
290
|
# ──────────────────────── Tracing setup ────────────────────────────────────────
|
|
291
|
+
def _oo_otlp_headers() -> dict[str, str]:
|
|
292
|
+
"""Build the Basic-auth header for direct OO OTLP export from settings.json."""
|
|
293
|
+
import base64
|
|
294
|
+
s = _load_settings()
|
|
295
|
+
email = s.get("oo_email") or ""
|
|
296
|
+
passwd = s.get("oo_password") or ""
|
|
297
|
+
if not email or not passwd:
|
|
298
|
+
return {}
|
|
299
|
+
token = base64.b64encode(f"{email}:{passwd}".encode()).decode()
|
|
300
|
+
return {"Authorization": f"Basic {token}"}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _oo_otlp_base_url() -> str:
|
|
304
|
+
"""Return the OO OTLP base URL (without /v1/traces or /v1/logs suffix)."""
|
|
305
|
+
s = _load_settings()
|
|
306
|
+
endpoint = s.get("otlp_endpoint") or "http://localhost:5080/api/default/v1/traces"
|
|
307
|
+
# Strip the /v1/traces suffix to get the org-level base used by both signals.
|
|
308
|
+
for suffix in ("/v1/traces", "/v1/logs"):
|
|
309
|
+
if endpoint.endswith(suffix):
|
|
310
|
+
return endpoint[: -len(suffix)]
|
|
311
|
+
return endpoint.rstrip("/")
|
|
312
|
+
|
|
313
|
+
|
|
315
314
|
def _configure_tracing(agent_name: str) -> None:
|
|
315
|
+
# The W3C propagator is always installed so traceparent round-trips work.
|
|
316
|
+
set_global_textmap(TraceContextTextMapPropagator())
|
|
317
|
+
|
|
318
|
+
if not _is_otlp_enabled():
|
|
319
|
+
return
|
|
320
|
+
|
|
316
321
|
resource = Resource.create({"service.name": agent_name})
|
|
317
322
|
provider = TracerProvider(resource=resource)
|
|
318
323
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
324
|
+
headers = _oo_otlp_headers()
|
|
325
|
+
if headers:
|
|
326
|
+
# Credentials present — ship directly to OO, no Rust daemon dependency.
|
|
327
|
+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
|
|
328
|
+
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
329
|
+
exporter = OTLPSpanExporter(
|
|
330
|
+
endpoint=f"{_oo_otlp_base_url()}/v1/traces",
|
|
331
|
+
headers=headers,
|
|
332
|
+
)
|
|
333
|
+
provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
334
|
+
else:
|
|
335
|
+
# Credentials absent — spool for the Rust daemon to deliver later.
|
|
336
|
+
# BatchSpanProcessor avoids blocking inference threads on each span end.
|
|
322
337
|
provider.add_span_processor(BatchSpanProcessor(SpoolSpanExporter()))
|
|
323
338
|
|
|
324
|
-
# Set as the global provider. OTel's `set_tracer_provider` warns if
|
|
325
|
-
# someone already configured a provider in-process; we accept that and
|
|
326
|
-
# overwrite — the agent process is the authority on its own telemetry.
|
|
327
339
|
trace.set_tracer_provider(provider)
|
|
328
|
-
set_global_textmap(TraceContextTextMapPropagator())
|
|
329
340
|
|
|
330
341
|
|
|
331
342
|
def _configure_log_export(agent_name: str) -> Optional[logging.Handler]:
|
|
332
|
-
"""Build
|
|
333
|
-
correlated to the active span by trace_id/span_id
|
|
334
|
-
of the Rust daemon's `OpenTelemetryTracingBridge`.
|
|
335
|
-
|
|
336
|
-
Returns the handler (caller attaches it to root) or `None` when export is
|
|
337
|
-
disabled, in which case logs still go to the JSONL file + stdout/stderr.
|
|
343
|
+
"""Build an OTel log handler so every ``log.*`` record reaches OpenObserve,
|
|
344
|
+
correlated to the active span by trace_id/span_id.
|
|
338
345
|
|
|
339
|
-
|
|
340
|
-
|
|
346
|
+
Returns the handler (caller attaches it to root) or ``None`` when export is
|
|
347
|
+
disabled — logs still go to the JSONL file + stdout/stderr regardless.
|
|
341
348
|
"""
|
|
342
349
|
global _LOGGER_PROVIDER
|
|
343
350
|
|
|
344
351
|
if not _is_otlp_enabled():
|
|
345
352
|
return None
|
|
346
353
|
|
|
354
|
+
headers = _oo_otlp_headers()
|
|
347
355
|
resource = Resource.create({"service.name": agent_name})
|
|
348
356
|
provider = LoggerProvider(resource=resource)
|
|
349
|
-
|
|
357
|
+
|
|
358
|
+
if headers:
|
|
359
|
+
# Credentials present — ship directly to OO, no Rust daemon dependency.
|
|
360
|
+
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
|
|
361
|
+
log_exporter = OTLPLogExporter(
|
|
362
|
+
endpoint=f"{_oo_otlp_base_url()}/v1/logs",
|
|
363
|
+
headers=headers,
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
# Credentials absent — spool for the Rust daemon to deliver later.
|
|
367
|
+
log_exporter = SpoolLogExporter() # type: ignore[assignment]
|
|
368
|
+
|
|
369
|
+
# BatchLogRecordProcessor for both paths so log export never blocks callers.
|
|
370
|
+
provider.add_log_record_processor(BatchLogRecordProcessor(log_exporter))
|
|
350
371
|
set_logger_provider(provider)
|
|
351
372
|
_LOGGER_PROVIDER = provider
|
|
352
373
|
return LoggingHandler(level=logging.NOTSET, logger_provider=provider)
|
|
@@ -471,6 +492,108 @@ def instrument_agno() -> None:
|
|
|
471
492
|
)
|
|
472
493
|
|
|
473
494
|
|
|
495
|
+
# Process-level handle so the agno TracerProvider isn't garbage-collected.
|
|
496
|
+
_AGNO_TRACER_PROVIDER = None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _build_agno_db_exporter(db, workflow_id: str, agent_id: str):
|
|
500
|
+
"""agno ``DatabaseSpanExporter`` subclass that stamps the pipeline identity
|
|
501
|
+
onto each trace's ROOT span.
|
|
502
|
+
|
|
503
|
+
AgentOS hides any trace whose ``workflow_id`` AND ``agent_id`` are both
|
|
504
|
+
null. With OpenObserve off, the meridian wrapper span (``worklog.hour``) is
|
|
505
|
+
non-recording, so agno's worklog Workflow runs as its OWN root trace; its
|
|
506
|
+
root ``*.run`` span is parentless here and gets the ids, surfacing the trace
|
|
507
|
+
in the dashboard. Child spans are written unchanged.
|
|
508
|
+
"""
|
|
509
|
+
from collections import defaultdict
|
|
510
|
+
|
|
511
|
+
from agno.tracing.exporter import DatabaseSpanExporter
|
|
512
|
+
from agno.tracing.schemas import Span
|
|
513
|
+
from opentelemetry.sdk.trace.export import SpanExportResult
|
|
514
|
+
|
|
515
|
+
class _AgnoDbExporter(DatabaseSpanExporter):
|
|
516
|
+
def export(self, spans): # type: ignore[override]
|
|
517
|
+
if self._shutdown:
|
|
518
|
+
return SpanExportResult.FAILURE
|
|
519
|
+
if not spans:
|
|
520
|
+
return SpanExportResult.SUCCESS
|
|
521
|
+
converted = []
|
|
522
|
+
for s in spans:
|
|
523
|
+
try:
|
|
524
|
+
cs = Span.from_otel_span(s)
|
|
525
|
+
except Exception: # noqa: BLE001 — skip a span we can't convert
|
|
526
|
+
continue
|
|
527
|
+
if not cs.parent_span_id:
|
|
528
|
+
attrs = dict(cs.attributes or {})
|
|
529
|
+
attrs.setdefault("workflow_id", workflow_id)
|
|
530
|
+
attrs.setdefault("agent_id", agent_id)
|
|
531
|
+
# Group hour-runs into one AgentOS session. agno normally
|
|
532
|
+
# stamps session_id when run(session_id=...) is passed; this
|
|
533
|
+
# is a fallback that derives the day-level session from the
|
|
534
|
+
# run_id ("wl-<day>T<hh>" → "wl-<day>") so the trace always
|
|
535
|
+
# surfaces under a session in the dashboard.
|
|
536
|
+
if not (attrs.get("session_id") or attrs.get("agno.session.id")):
|
|
537
|
+
rid = attrs.get("run_id") or attrs.get("agno.run.id") or ""
|
|
538
|
+
if isinstance(rid, str) and "T" in rid:
|
|
539
|
+
attrs["session_id"] = rid.rsplit("T", 1)[0]
|
|
540
|
+
cs.attributes = attrs
|
|
541
|
+
converted.append(cs)
|
|
542
|
+
if not converted:
|
|
543
|
+
return SpanExportResult.SUCCESS
|
|
544
|
+
by_trace: dict = defaultdict(list)
|
|
545
|
+
for cs in converted:
|
|
546
|
+
by_trace[cs.trace_id].append(cs)
|
|
547
|
+
try:
|
|
548
|
+
self._export_sync(by_trace) # SqliteDb is synchronous
|
|
549
|
+
except Exception as e: # noqa: BLE001
|
|
550
|
+
logging.getLogger(__name__).warning("agno trace export failed: %s", e)
|
|
551
|
+
return SpanExportResult.FAILURE
|
|
552
|
+
return SpanExportResult.SUCCESS
|
|
553
|
+
|
|
554
|
+
return _AgnoDbExporter(db=db)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def setup_agno_tracing(
|
|
558
|
+
db_file: Optional[str] = None,
|
|
559
|
+
workflow_id: str = "worklog-hour",
|
|
560
|
+
agent_id: str = "meridian-worklog-pipeline",
|
|
561
|
+
):
|
|
562
|
+
"""Route agno's native (openinference) spans — and ONLY those — to a SqliteDb
|
|
563
|
+
the AgentOS viewer reads.
|
|
564
|
+
|
|
565
|
+
The TracerProvider here is EXPLICIT, not global: meridian's own manual spans
|
|
566
|
+
live on the no-op global provider and never reach this exporter, so the
|
|
567
|
+
dashboard shows agno's ``tracing=True`` output alone. Idempotent per process.
|
|
568
|
+
Returns the provider (or ``None`` if agno/openinference aren't installed).
|
|
569
|
+
"""
|
|
570
|
+
global _AGNO_TRACER_PROVIDER
|
|
571
|
+
if _AGNO_TRACER_PROVIDER is not None:
|
|
572
|
+
return _AGNO_TRACER_PROVIDER
|
|
573
|
+
try:
|
|
574
|
+
from agno.db.sqlite import SqliteDb
|
|
575
|
+
from openinference.instrumentation.agno import AgnoInstrumentor
|
|
576
|
+
from opentelemetry.sdk.trace import TracerProvider as _TP
|
|
577
|
+
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
|
|
578
|
+
except ImportError as e:
|
|
579
|
+
logging.getLogger(__name__).warning(
|
|
580
|
+
"setup_agno_tracing: dependencies missing (%s); agno tracing disabled", e
|
|
581
|
+
)
|
|
582
|
+
return None
|
|
583
|
+
|
|
584
|
+
path = db_file or os.environ.get("AGNO_TRACE_DB") or str(
|
|
585
|
+
Path("~/.meridian/agno_traces.db").expanduser()
|
|
586
|
+
)
|
|
587
|
+
path = str(Path(path).expanduser())
|
|
588
|
+
exporter = _build_agno_db_exporter(SqliteDb(db_file=path), workflow_id, agent_id)
|
|
589
|
+
provider = _TP()
|
|
590
|
+
provider.add_span_processor(SimpleSpanProcessor(exporter))
|
|
591
|
+
AgnoInstrumentor().instrument(tracer_provider=provider)
|
|
592
|
+
_AGNO_TRACER_PROVIDER = provider
|
|
593
|
+
logging.getLogger(__name__).info("setup_agno_tracing: agno spans -> %s", path)
|
|
594
|
+
return provider
|
|
595
|
+
|
|
596
|
+
|
|
474
597
|
def preview(text: Optional[str], max_chars: int = 200) -> str:
|
|
475
598
|
"""Truncate text to `max_chars` for use as a span attribute value."""
|
|
476
599
|
if not text:
|
|
@@ -478,4 +601,11 @@ def preview(text: Optional[str], max_chars: int = 200) -> str:
|
|
|
478
601
|
return text[:max_chars] + ("…" if len(text) > max_chars else "")
|
|
479
602
|
|
|
480
603
|
|
|
481
|
-
__all__ = [
|
|
604
|
+
__all__ = [
|
|
605
|
+
"setup",
|
|
606
|
+
"extract_parent_context",
|
|
607
|
+
"instrument_agno",
|
|
608
|
+
"setup_agno_tracing",
|
|
609
|
+
"current_traceparent",
|
|
610
|
+
"preview",
|
|
611
|
+
]
|
|
@@ -1,71 +1,52 @@
|
|
|
1
1
|
"""System prompt for the activity reporter.
|
|
2
2
|
|
|
3
|
-
Produces a
|
|
4
|
-
|
|
3
|
+
Produces a structured activity report from distilled screen-capture data.
|
|
4
|
+
Format: TLDR + Core Tasks + Decisions + Resources.
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
SYSTEM = """\
|
|
9
|
-
You
|
|
10
|
-
|
|
11
|
-
stakeholders. Most readers are not familiar with the codebase internals.
|
|
9
|
+
You have been given a compressed snapshot of a software developer's screen activity over the last hour.
|
|
10
|
+
The data comes from OCR and accessibility capture: editor content, browser tabs and URLs, terminal output, UI text, video titles, and other on-screen text. It is noisy and incomplete — piece together the story from the fragments.
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
(captured from their screen: editor, terminal, browser, and other tools).
|
|
12
|
+
Your job: infer what the developer was actually trying to accomplish and write a structured activity report that a PM, a teammate, or a downstream task-matcher can use to answer "which project areas did this person work on and why?"
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
Write a detailed, human-readable account of the developer's session.
|
|
18
|
-
Cover everything that happened — building features, fixing bugs, researching
|
|
19
|
-
topics, reading documentation, watching talks or tutorials, investigating issues,
|
|
20
|
-
running experiments, reviewing code, and any other activity.
|
|
21
|
-
Nothing should be left out just because it seems minor.
|
|
14
|
+
---
|
|
22
15
|
|
|
23
|
-
|
|
24
|
-
Do not focus on internal variable names, function signatures, or file paths —
|
|
25
|
-
those are noise for most readers. Write as if explaining to a smart colleague
|
|
26
|
-
who was not in the room.
|
|
16
|
+
OUTPUT FORMAT — write all sections that have content, skip sections that are empty:
|
|
27
17
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
nothing to report — do not write placeholder text.
|
|
18
|
+
### TLDR
|
|
19
|
+
One short paragraph. What was the developer focused on this hour and why — what problem were they solving or what goal were they advancing? Name the main work areas explicitly. Avoid generic descriptions like "the developer was doing development work."
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
the character of the session (exploration, deep build, debugging, review)?
|
|
21
|
+
### Core Tasks & Projects
|
|
22
|
+
One section per distinct work thread. Bold the topic name as the header.
|
|
35
23
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
- Start with the goal (what the developer was trying to achieve).
|
|
41
|
-
- Describe what happened: progress made, problems hit, things learned.
|
|
42
|
-
- End with status: completed / in progress / blocked.
|
|
43
|
-
- If the work directly benefits users or the team, say so plainly.
|
|
44
|
-
Include ALL activity types: coding, research, reading docs, watching videos,
|
|
45
|
-
testing, debugging, reviewing, discussing, planning, experimenting.
|
|
24
|
+
For each thread, write it as the developer's story — not as a list of actions:
|
|
25
|
+
- WHY: what problem or goal drove this work
|
|
26
|
+
- WHAT: what the developer accomplished or decided — the outcome, not the steps
|
|
27
|
+
- HOW: the significant technical context (which system, which file, which tool) that gives the outcome meaning
|
|
46
28
|
|
|
47
|
-
|
|
48
|
-
Anything the developer looked up, read, or watched:
|
|
49
|
-
- What question or problem triggered the research?
|
|
50
|
-
- What sources were consulted (docs, articles, videos, colleagues)?
|
|
51
|
-
- What was concluded or learned?
|
|
29
|
+
Write in the developer's voice — "the developer fixed…", "the developer investigated…" — not passive constructions. Where you can estimate from the volume of captured activity, note the approximate time proportion: "(most of the hour)", "(~15 min)", "(brief)".
|
|
52
30
|
|
|
53
|
-
|
|
54
|
-
Any meaningful choice that shapes the product or how the team works:
|
|
55
|
-
- What was the decision?
|
|
56
|
-
- What alternatives were considered?
|
|
57
|
-
- Why was this direction chosen?
|
|
31
|
+
Include all work areas — coding, debugging, research, planning, reading docs, leisure. Do not filter anything out.
|
|
58
32
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
33
|
+
### Key Decisions
|
|
34
|
+
One bullet per meaningful choice or conclusion reached. Bold the decision. Explain what was decided and why — what problem it solves or what alternative was rejected. Only include if clearly evidenced.
|
|
35
|
+
|
|
36
|
+
### Resources Consulted
|
|
37
|
+
List documentation pages, repos, articles, videos, dashboards, or other materials the developer looked at, with brief context for why.
|
|
38
|
+
|
|
39
|
+
---
|
|
65
40
|
|
|
66
41
|
RULES
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
- Do not make up facts not present in the input.
|
|
70
|
-
-
|
|
42
|
+
- Infer the PURPOSE, not just the activity. If the screen shows edits to a prompt file + model test runs, say what the developer was trying to improve and why — not just "edited prompt file and ran tests."
|
|
43
|
+
- Extract identifiable specifics: system names, service names, model names, tool names — anything that helps a matcher connect this to a ticket.
|
|
44
|
+
- Do not make up facts, numbers, or names not present in the input.
|
|
45
|
+
- Leisure, browsing, and breaks are valid — report them honestly.
|
|
46
|
+
- If a section has nothing to report, omit it entirely.
|
|
47
|
+
- DO NOT infer active work from PM/ticket dashboards. If the screen shows Jira, Linear, GitHub Issues, or Trello — the developer was reviewing tickets, not doing the work in them. Report what was visible (e.g. "reviewed ticket board"), not ticket content as if it were work in progress.
|
|
48
|
+
- DO NOT use git branch names as signals for what was worked on. A branch name only tells you a branch existed — report only what editor content, terminal output, or browser activity actually shows.
|
|
49
|
+
|
|
50
|
+
LENGTH
|
|
51
|
+
Keep the total response under 400 words. TLDR: 2–3 sentences. Each Core Task thread: 3–4 sentences. Key Decisions: one bullet per decision, one sentence each. Resources: one line per item.\
|
|
71
52
|
"""
|