power-loop 3.7.0__tar.gz → 3.8.1__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.
- {power_loop-3.7.0 → power_loop-3.8.1}/PKG-INFO +1 -1
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/__init__.py +1 -1
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/agent/sink.py +3 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/dialect.py +6 -3
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/schema.py +19 -1
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/store.py +12 -7
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/types.py +3 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/tools/default_tools.py +15 -2
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop.egg-info/PKG-INFO +1 -1
- {power_loop-3.7.0 → power_loop-3.8.1}/LICENSE +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/README.md +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/_vendor/__init__.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/_vendor/llm_client/__init__.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/_vendor/llm_client/anthropic_factory.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/_vendor/llm_client/capabilities.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/_vendor/llm_client/interface.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/_vendor/llm_client/llm_factory.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/_vendor/llm_client/llm_tooling.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/_vendor/llm_client/llm_utils.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/_vendor/llm_client/multimodal.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/agent/__init__.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/agent/follow_up.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/agent/stateful_loop.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/agent/system_prompt.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/agent/types.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contracts/__init__.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contracts/errors.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contracts/event_payloads.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contracts/events.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contracts/handlers.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contracts/hook_contexts.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contracts/hooks.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contracts/messages.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contracts/protocols.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contracts/tools.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contrib/__init__.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contrib/_redact.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contrib/jsonl_sink.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contrib/logging_sink.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contrib/mcp.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contrib/metrics_sink.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/contrib/otel_sink.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/core/agent_context.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/core/events.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/core/hooks.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/core/phase.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/core/pipeline.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/core/runner.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/core/state.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/py.typed +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/blackboard.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/budget.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/cancellation.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/compact.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/env.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/exec_backend.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/fold.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/fold_adapter.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/history_projector.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/history_sanitize.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/human_input.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/memory.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/notes.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/provider.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/representation.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/retry.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/runtime_state.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/session_store.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/skills.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/spec.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/__init__.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/backends/__init__.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/backends/mysql.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/backends/postgres.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/backends/sqlite.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/capabilities.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/db.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/store/factory.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/structured.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/stub_provider.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/runtime/timers.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/tools/__init__.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/tools/blackboard.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/tools/default_manifest.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/tools/registry.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/tools/spawn_agent.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/__init__.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/api.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/engine.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/introspect.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/journal.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/result.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/resume.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/runner.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/spec.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/subprocess_executor.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/tool.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop/workflow/worker.py +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop.egg-info/SOURCES.txt +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop.egg-info/dependency_links.txt +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop.egg-info/requires.txt +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/power_loop.egg-info/top_level.txt +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/pyproject.toml +0 -0
- {power_loop-3.7.0 → power_loop-3.8.1}/setup.cfg +0 -0
|
@@ -15,7 +15,7 @@ Stability tiers
|
|
|
15
15
|
无版本承诺,可随时变更或删除。
|
|
16
16
|
"""
|
|
17
17
|
|
|
18
|
-
__version__ = "3.
|
|
18
|
+
__version__ = "3.8.1"
|
|
19
19
|
|
|
20
20
|
# Public LLM contract (SDK-free) re-exported so callers (e.g. writing llm.* hooks or
|
|
21
21
|
# a custom LLMService) don't reach into the internal vendored transport package (H3.4).
|
|
@@ -377,6 +377,9 @@ class SQLiteSink:
|
|
|
377
377
|
usage.get("completion_tokens") or usage.get("output")
|
|
378
378
|
),
|
|
379
379
|
total_tokens=_int_or_none(usage.get("total_tokens")),
|
|
380
|
+
cached_tokens=_int_or_none(
|
|
381
|
+
usage.get("cache_read_tokens") or usage.get("cached_tokens")
|
|
382
|
+
),
|
|
380
383
|
)
|
|
381
384
|
|
|
382
385
|
|
|
@@ -101,7 +101,7 @@ class SqliteDialect:
|
|
|
101
101
|
PRIMARY KEY (session_id, compact_seq))""",
|
|
102
102
|
f"""CREATE TABLE IF NOT EXISTS {p}usage_rounds (
|
|
103
103
|
session_id TEXT NOT NULL, round_index INTEGER NOT NULL, prompt_tokens INTEGER,
|
|
104
|
-
completion_tokens INTEGER, total_tokens INTEGER, model TEXT,
|
|
104
|
+
completion_tokens INTEGER, total_tokens INTEGER, cached_tokens INTEGER, model TEXT,
|
|
105
105
|
created_at INTEGER NOT NULL, PRIMARY KEY (session_id, round_index))""",
|
|
106
106
|
f"""CREATE TABLE IF NOT EXISTS {p}session_state (
|
|
107
107
|
session_id TEXT PRIMARY KEY, next_seq INTEGER NOT NULL DEFAULT 1,
|
|
@@ -125,6 +125,7 @@ class SqliteDialect:
|
|
|
125
125
|
rounds INTEGER NOT NULL DEFAULT 0, llm_calls INTEGER NOT NULL DEFAULT 0,
|
|
126
126
|
tool_calls INTEGER NOT NULL DEFAULT 0, prompt_tokens INTEGER NOT NULL DEFAULT 0,
|
|
127
127
|
completion_tokens INTEGER NOT NULL DEFAULT 0, total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
128
|
+
cached_tokens INTEGER NOT NULL DEFAULT 0,
|
|
128
129
|
first_send_at INTEGER, last_send_at INTEGER, updated_at INTEGER NOT NULL)""",
|
|
129
130
|
f"""CREATE TABLE IF NOT EXISTS {p}timers (
|
|
130
131
|
session_id TEXT NOT NULL, timer_id INTEGER NOT NULL, due_at INTEGER NOT NULL,
|
|
@@ -243,7 +244,7 @@ class PostgresDialect:
|
|
|
243
244
|
PRIMARY KEY (session_id, compact_seq))""",
|
|
244
245
|
f"""CREATE TABLE IF NOT EXISTS {p}usage_rounds (
|
|
245
246
|
session_id TEXT NOT NULL, round_index BIGINT NOT NULL, prompt_tokens BIGINT,
|
|
246
|
-
completion_tokens BIGINT, total_tokens BIGINT, model TEXT,
|
|
247
|
+
completion_tokens BIGINT, total_tokens BIGINT, cached_tokens BIGINT, model TEXT,
|
|
247
248
|
created_at BIGINT NOT NULL, PRIMARY KEY (session_id, round_index))""",
|
|
248
249
|
f"""CREATE TABLE IF NOT EXISTS {p}session_state (
|
|
249
250
|
session_id TEXT PRIMARY KEY, next_seq BIGINT NOT NULL DEFAULT 1,
|
|
@@ -267,6 +268,7 @@ class PostgresDialect:
|
|
|
267
268
|
rounds BIGINT NOT NULL DEFAULT 0, llm_calls BIGINT NOT NULL DEFAULT 0,
|
|
268
269
|
tool_calls BIGINT NOT NULL DEFAULT 0, prompt_tokens BIGINT NOT NULL DEFAULT 0,
|
|
269
270
|
completion_tokens BIGINT NOT NULL DEFAULT 0, total_tokens BIGINT NOT NULL DEFAULT 0,
|
|
271
|
+
cached_tokens BIGINT NOT NULL DEFAULT 0,
|
|
270
272
|
first_send_at BIGINT, last_send_at BIGINT, updated_at BIGINT NOT NULL)""",
|
|
271
273
|
f"""CREATE TABLE IF NOT EXISTS {p}timers (
|
|
272
274
|
session_id TEXT NOT NULL, timer_id BIGINT NOT NULL, due_at BIGINT NOT NULL,
|
|
@@ -372,7 +374,7 @@ class MySQLDialect:
|
|
|
372
374
|
PRIMARY KEY (session_id, compact_seq)) {opts}""",
|
|
373
375
|
f"""CREATE TABLE IF NOT EXISTS {p}usage_rounds (
|
|
374
376
|
session_id VARCHAR(255) NOT NULL, round_index BIGINT NOT NULL, prompt_tokens BIGINT,
|
|
375
|
-
completion_tokens BIGINT, total_tokens BIGINT, model VARCHAR(255),
|
|
377
|
+
completion_tokens BIGINT, total_tokens BIGINT, cached_tokens BIGINT, model VARCHAR(255),
|
|
376
378
|
created_at BIGINT NOT NULL, PRIMARY KEY (session_id, round_index)) {opts}""",
|
|
377
379
|
f"""CREATE TABLE IF NOT EXISTS {p}session_state (
|
|
378
380
|
session_id VARCHAR(255) NOT NULL, next_seq BIGINT NOT NULL DEFAULT 1,
|
|
@@ -395,6 +397,7 @@ class MySQLDialect:
|
|
|
395
397
|
rounds BIGINT NOT NULL DEFAULT 0, llm_calls BIGINT NOT NULL DEFAULT 0,
|
|
396
398
|
tool_calls BIGINT NOT NULL DEFAULT 0, prompt_tokens BIGINT NOT NULL DEFAULT 0,
|
|
397
399
|
completion_tokens BIGINT NOT NULL DEFAULT 0, total_tokens BIGINT NOT NULL DEFAULT 0,
|
|
400
|
+
cached_tokens BIGINT NOT NULL DEFAULT 0,
|
|
398
401
|
first_send_at BIGINT, last_send_at BIGINT, updated_at BIGINT NOT NULL,
|
|
399
402
|
PRIMARY KEY (session_id)) {opts}""",
|
|
400
403
|
f"""CREATE TABLE IF NOT EXISTS {p}timers (
|
|
@@ -67,7 +67,9 @@ def validate_table_prefix(prefix: str) -> str:
|
|
|
67
67
|
#: Bump + append a migration step for ANY schema change.
|
|
68
68
|
#: v2 (2026-06): adds the ``{prefix}project_messages`` table (send-context projection).
|
|
69
69
|
#: v3 (2026-06): adds the ``{prefix}hook_events`` table (ephemeral hook-augmentation audit log).
|
|
70
|
-
|
|
70
|
+
#: v4 (2026-06): widen MySQL free-text/JSON columns TEXT→LONGTEXT.
|
|
71
|
+
#: v5 (2026-06): adds ``cached_tokens`` (prompt cache-read tokens) to usage_rounds + session_stats.
|
|
72
|
+
CURRENT_SCHEMA_VERSION = 5
|
|
71
73
|
|
|
72
74
|
#: The store's data tables (besides ``{prefix}schema_migrations``) — used by VERIFY to
|
|
73
75
|
#: confirm the FULL schema is present, not just the version row. Keep in sync with
|
|
@@ -118,6 +120,16 @@ async def _migration_steps(
|
|
|
118
120
|
# idempotent. (Fresh stores already provision LONGTEXT via Dialect.ddl.)
|
|
119
121
|
if db.dialect.name == "mysql":
|
|
120
122
|
steps += db.dialect.widen_text_columns_ddl(prefix)
|
|
123
|
+
if from_version < 5:
|
|
124
|
+
# v4 → v5: add cached_tokens (prompt cache-read tokens) to usage_rounds + session_stats.
|
|
125
|
+
# ALTER … ADD COLUMN has no portable IF NOT EXISTS, so probe the catalog on the open tx.
|
|
126
|
+
int_t = "INTEGER" if db.dialect.name == "sqlite" else "BIGINT"
|
|
127
|
+
if not await _column_exists(tx, db.dialect.name, f"{prefix}usage_rounds", "cached_tokens"):
|
|
128
|
+
steps.append(f"ALTER TABLE {prefix}usage_rounds ADD COLUMN cached_tokens {int_t}")
|
|
129
|
+
if not await _column_exists(tx, db.dialect.name, f"{prefix}session_stats", "cached_tokens"):
|
|
130
|
+
steps.append(
|
|
131
|
+
f"ALTER TABLE {prefix}session_stats ADD COLUMN cached_tokens {int_t} NOT NULL DEFAULT 0"
|
|
132
|
+
)
|
|
121
133
|
return steps
|
|
122
134
|
|
|
123
135
|
|
|
@@ -137,6 +149,12 @@ def migration_ddl_for_display(db: Database, prefix: str, *, from_version: int) -
|
|
|
137
149
|
steps += db.dialect.hook_events_ddl(prefix)
|
|
138
150
|
if from_version < 4 and db.dialect.name == "mysql":
|
|
139
151
|
steps += db.dialect.widen_text_columns_ddl(prefix)
|
|
152
|
+
if from_version < 5:
|
|
153
|
+
int_t = "INTEGER" if db.dialect.name == "sqlite" else "BIGINT"
|
|
154
|
+
steps.append(f"ALTER TABLE {prefix}usage_rounds ADD COLUMN cached_tokens {int_t}")
|
|
155
|
+
steps.append(
|
|
156
|
+
f"ALTER TABLE {prefix}session_stats ADD COLUMN cached_tokens {int_t} NOT NULL DEFAULT 0"
|
|
157
|
+
)
|
|
140
158
|
return steps
|
|
141
159
|
|
|
142
160
|
|
|
@@ -146,7 +146,7 @@ _EXPORT_TABLES: tuple[tuple[str, Any, tuple[str, ...]], ...] = (
|
|
|
146
146
|
"after_tokens", "round_index", "created_at")),
|
|
147
147
|
("usage_rounds", lambda t: t.usage_rounds, (
|
|
148
148
|
"session_id", "round_index", "prompt_tokens", "completion_tokens", "total_tokens",
|
|
149
|
-
"model", "created_at")),
|
|
149
|
+
"cached_tokens", "model", "created_at")),
|
|
150
150
|
("session_runtime_state", lambda t: t.session_runtime_state, (
|
|
151
151
|
"session_id", "state_key", "value_json", "updated_at")),
|
|
152
152
|
("timers", lambda t: t.timers, (
|
|
@@ -156,7 +156,8 @@ _EXPORT_TABLES: tuple[tuple[str, Any, tuple[str, ...]], ...] = (
|
|
|
156
156
|
"session_id", "note_id", "content", "pinned", "created_at", "updated_at")),
|
|
157
157
|
("session_stats", lambda t: t.session_stats, (
|
|
158
158
|
"session_id", "sends", "rounds", "llm_calls", "tool_calls", "prompt_tokens",
|
|
159
|
-
"completion_tokens", "total_tokens", "
|
|
159
|
+
"completion_tokens", "total_tokens", "cached_tokens", "first_send_at", "last_send_at",
|
|
160
|
+
"updated_at")),
|
|
160
161
|
)
|
|
161
162
|
|
|
162
163
|
|
|
@@ -1143,6 +1144,7 @@ class SessionStore:
|
|
|
1143
1144
|
prompt_tokens: int | None,
|
|
1144
1145
|
completion_tokens: int | None,
|
|
1145
1146
|
total_tokens: int | None,
|
|
1147
|
+
cached_tokens: int | None = None,
|
|
1146
1148
|
model: str | None = None,
|
|
1147
1149
|
) -> None:
|
|
1148
1150
|
"""Record per-round token usage. Legacy used ``INSERT OR REPLACE`` keyed on
|
|
@@ -1151,14 +1153,15 @@ class SessionStore:
|
|
|
1151
1153
|
sql = self._db.dialect.upsert(
|
|
1152
1154
|
self.t.usage_rounds,
|
|
1153
1155
|
("session_id", "round_index"),
|
|
1154
|
-
("prompt_tokens", "completion_tokens", "total_tokens", "
|
|
1156
|
+
("prompt_tokens", "completion_tokens", "total_tokens", "cached_tokens", "model",
|
|
1157
|
+
"created_at"),
|
|
1155
1158
|
)
|
|
1156
1159
|
async with self._db.transaction() as tx:
|
|
1157
1160
|
await tx.execute(
|
|
1158
1161
|
sql,
|
|
1159
1162
|
(
|
|
1160
1163
|
session_id, round_index, prompt_tokens, completion_tokens,
|
|
1161
|
-
total_tokens, model, _now_ms(),
|
|
1164
|
+
total_tokens, cached_tokens, model, _now_ms(),
|
|
1162
1165
|
),
|
|
1163
1166
|
)
|
|
1164
1167
|
|
|
@@ -1172,7 +1175,7 @@ class SessionStore:
|
|
|
1172
1175
|
tool_calls: int = 0,
|
|
1173
1176
|
) -> None:
|
|
1174
1177
|
"""Cumulative per-session accounting, bumped once per finished send. On conflict
|
|
1175
|
-
the
|
|
1178
|
+
the eight counters ACCUMULATE (``col = col + new``), ``last_send_at``/``updated_at``
|
|
1176
1179
|
OVERWRITE, and ``first_send_at`` is INSERT-ONLY (preserved on conflict, matching
|
|
1177
1180
|
legacy which omits it from the ON CONFLICT SET). ``sends`` accumulates by 1."""
|
|
1178
1181
|
now = _now_ms()
|
|
@@ -1182,7 +1185,7 @@ class SessionStore:
|
|
|
1182
1185
|
("last_send_at", "updated_at"),
|
|
1183
1186
|
add_cols=(
|
|
1184
1187
|
"sends", "rounds", "llm_calls", "tool_calls",
|
|
1185
|
-
"prompt_tokens", "completion_tokens", "total_tokens",
|
|
1188
|
+
"prompt_tokens", "completion_tokens", "total_tokens", "cached_tokens",
|
|
1186
1189
|
),
|
|
1187
1190
|
insert_only_cols=("first_send_at",),
|
|
1188
1191
|
)
|
|
@@ -1198,6 +1201,7 @@ class SessionStore:
|
|
|
1198
1201
|
int(usage.get("prompt_tokens") or 0), # add: prompt_tokens
|
|
1199
1202
|
int(usage.get("completion_tokens") or 0), # add: completion_tokens
|
|
1200
1203
|
int(usage.get("total_tokens") or 0), # add: total_tokens
|
|
1204
|
+
int(usage.get("cache_read_tokens") or usage.get("cached_tokens") or 0), # add: cached_tokens
|
|
1201
1205
|
now, # insert-only: first_send_at
|
|
1202
1206
|
)
|
|
1203
1207
|
async with self._db.transaction() as tx:
|
|
@@ -1650,7 +1654,8 @@ def _row_to_stats(row: Row) -> SessionStatsRow:
|
|
|
1650
1654
|
session_id=row["session_id"], sends=row["sends"], rounds=row["rounds"],
|
|
1651
1655
|
llm_calls=row["llm_calls"], tool_calls=row["tool_calls"],
|
|
1652
1656
|
prompt_tokens=row["prompt_tokens"], completion_tokens=row["completion_tokens"],
|
|
1653
|
-
total_tokens=row["total_tokens"],
|
|
1657
|
+
total_tokens=row["total_tokens"], cached_tokens=row["cached_tokens"],
|
|
1658
|
+
first_send_at=row["first_send_at"],
|
|
1654
1659
|
last_send_at=row["last_send_at"], updated_at=row["updated_at"],
|
|
1655
1660
|
)
|
|
1656
1661
|
|
|
@@ -97,6 +97,9 @@ class SessionStatsRow:
|
|
|
97
97
|
first_send_at: int | None
|
|
98
98
|
last_send_at: int | None
|
|
99
99
|
updated_at: int
|
|
100
|
+
#: Prompt cache-read tokens (v5). Default 0 so the legacy SQLite store (which doesn't track it)
|
|
101
|
+
#: still constructs the row.
|
|
102
|
+
cached_tokens: int = 0
|
|
100
103
|
|
|
101
104
|
|
|
102
105
|
@dataclass
|
|
@@ -124,8 +124,21 @@ def _looks_binary(data: bytes) -> bool:
|
|
|
124
124
|
if not data:
|
|
125
125
|
return False
|
|
126
126
|
sample = data[:4096]
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
# Fast path: mostly-ASCII text (incl. common whitespace) is clearly text.
|
|
128
|
+
ascii_ish = sum(byte in b"\n\r\t\b\f" or 32 <= byte <= 126 for byte in sample)
|
|
129
|
+
if ascii_ish / len(sample) >= 0.70:
|
|
130
|
+
return False
|
|
131
|
+
# Otherwise it may be NON-Latin UTF-8 text (CJK / emoji, whose bytes are mostly >126) —
|
|
132
|
+
# NOT binary. Decode leniently (a multibyte char truncated at the 4KB sample boundary is
|
|
133
|
+
# negligible) and flag as binary only if the text is littered with undecodable bytes
|
|
134
|
+
# (U+FFFD) or C0/C1 control chars (excluding normal whitespace), which real text lacks.
|
|
135
|
+
text = sample.decode("utf-8", errors="replace")
|
|
136
|
+
bad = sum(
|
|
137
|
+
1
|
|
138
|
+
for ch in text
|
|
139
|
+
if ch == "�" or ord(ch) == 0x7f or (ord(ch) < 32 and ch not in "\n\r\t\f\b")
|
|
140
|
+
)
|
|
141
|
+
return bad / len(text) > 0.15
|
|
129
142
|
|
|
130
143
|
|
|
131
144
|
def _read_text(fp: Path, *, max_bytes: int | None = TEXT_FILE_MAX_BYTES) -> str:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|