cloud: production control plane (API, auth/RBAC, queue, sandbox, memory/KB, fallback/cost, webhooks)

This commit is contained in:
host committed 2026-10-26 01:23:21 +08:00
1 parent b97c34465f
commit b74d99e959
17 files changed
+3420

No files matched your search

+144
View File
@@ -0,0 +1,144 @@
"""Project memory store: short task summaries for prompt context.
Zero third-party dependencies; Python standard library only.
Design notes:
- Summaries are the durable, compacted memory of a project. They are
deliberately short (goal / conclusion / files changed / open issues)
so that a prompt can carry recent context without replaying history.
- ``build_prompt_context`` never returns the whole history: it returns
at most ``top_k`` recent active summaries, formatted as a prompt prefix.
- ``deprecate`` is a soft mark (status change, not deletion); ``delete``
is a physical removal reserved for user-initiated correction/forgetting.
"""
import json
import sqlite3
import time
class MemoryStore:
"""SQLite-backed store for per-project task summaries."""
def __init__(self, db_path):
self.db_path = db_path
self.conn = sqlite3.connect(db_path)
self.conn.row_factory = sqlite3.Row
self.conn.execute(
"""CREATE TABLE IF NOT EXISTS summaries(
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id TEXT,
thread_id TEXT,
goal TEXT,
conclusion TEXT,
files_changed TEXT,
open_issues TEXT,
status TEXT DEFAULT 'active',
created_at REAL
)"""
)
self.conn.commit()
def add_summary(self, project_id, thread_id, goal, conclusion,
files_changed=None, open_issues=None):
"""Insert a summary; returns the new row id."""
cur = self.conn.execute(
"""INSERT INTO summaries
(project_id, thread_id, goal, conclusion,
files_changed, open_issues, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, 'active', ?)""",
(
project_id,
thread_id,
goal,
conclusion,
json.dumps(list(files_changed) if files_changed is not None else []),
json.dumps(list(open_issues) if open_issues is not None else []),
time.time(),
),
)
self.conn.commit()
return cur.lastrowid
def list_summaries(self, project_id, include_deprecated=False):
"""List summaries for one project, newest first.
By default only ``status='active'`` rows are returned; pass
``include_deprecated=True`` to also see deprecated ones.
"""
if include_deprecated:
rows = self.conn.execute(
"""SELECT * FROM summaries WHERE project_id = ?
ORDER BY created_at DESC, id DESC""",
(project_id,),
).fetchall()
else:
rows = self.conn.execute(
"""SELECT * FROM summaries WHERE project_id = ?
AND status = 'active'
ORDER BY created_at DESC, id DESC""",
(project_id,),
).fetchall()
return [self._row_to_dict(row) for row in rows]
def deprecate(self, summary_id):
"""Mark a summary deprecated (soft mark, not a physical delete)."""
self.conn.execute(
"UPDATE summaries SET status = 'deprecated' WHERE id = ?",
(summary_id,),
)
self.conn.commit()
def delete(self, summary_id):
"""Physically delete a summary.
Deletion is a user-initiated action (correction / forgetting):
it is the only way a summary is removed from storage, as opposed
to :meth:`deprecate`, which merely hides it from default listings
and prompt context while keeping the record.
"""
self.conn.execute(
"DELETE FROM summaries WHERE id = ?",
(summary_id,),
)
self.conn.commit()
def build_prompt_context(self, project_id, top_k=5):
"""Build a prompt prefix from the newest ``top_k`` active summaries.
Each summary becomes one line::
- [thread_id] 目标:...;结论:...;改动文件:...;遗留:...
Returns an empty string when the project has no active summaries.
Never returns the full history: only the newest ``top_k`` rows
(see :meth:`list_summaries`) are included.
"""
summaries = self.list_summaries(project_id)[:top_k]
if not summaries:
return ""
lines = []
for s in summaries:
files = "、".join(s["files_changed"]) if s["files_changed"] else "无"
issues = "、".join(s["open_issues"]) if s["open_issues"] else "无"
lines.append(
"- [{thread_id}] 目标:{goal};结论:{conclusion};"
"改动文件:{files};遗留:{issues}".format(
thread_id=s["thread_id"],
goal=s["goal"],
conclusion=s["conclusion"],
files=files,
issues=issues,
)
)
return "\n".join(lines)
@staticmethod
def _row_to_dict(row):
d = dict(row)
for key in ("files_changed", "open_issues"):
try:
d[key] = json.loads(d[key]) if d[key] else []
except (TypeError, ValueError):
d[key] = []
return d