Профиль агента: studio (Коди, мандаты без изменений) | client — агент клиента Shkor Labs Autopilot со своим именем, владельцем и инструментами тарифа (SIMBIOT_PROFILE, SIMBIOT_AGENT_DISPLAY_NAME, SIMBIOT_OWNER_NAME, SIMBIOT_COMPANY) + тест
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3d8b743d7a
commit
578856081e
3 changed files with 136 additions and 2 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
Контекст ∝ масштабу: чем крупнее цикл, тем больше контекст и модель.
|
Контекст ∝ масштабу: чем крупнее цикл, тем больше контекст и модель.
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field, replace
|
||||||
import os
|
import os
|
||||||
|
|
||||||
SEC = 1
|
SEC = 1
|
||||||
|
|
@ -15,6 +15,22 @@ QUARTER = 91 * DAY
|
||||||
YEAR = 366 * DAY # округляем год как 366 дней для простоты выравнивания
|
YEAR = 366 * DAY # округляем год как 366 дней для простоты выравнивания
|
||||||
|
|
||||||
|
|
||||||
|
def _env_cfg(key: str, default: str = "") -> str:
|
||||||
|
"""Значение настройки: env сильнее .env харнеса (мини-ридер без circular import)."""
|
||||||
|
val = os.environ.get(key, "").strip()
|
||||||
|
if val:
|
||||||
|
return val
|
||||||
|
try:
|
||||||
|
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
|
||||||
|
with open(env_path, encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith(key + "="):
|
||||||
|
return line.split("=", 1)[1].strip().strip('"').strip("'").strip() or default
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def _cfg_model(default: str = "auto") -> str:
|
def _cfg_model(default: str = "auto") -> str:
|
||||||
"""SIMBIOT_MODEL (визард первого запуска, t-261002193628-1d1e5f):
|
"""SIMBIOT_MODEL (визард первого запуска, t-261002193628-1d1e5f):
|
||||||
одна модель для всех LLM-уровней вместо auto — env сильнее .env
|
одна модель для всех LLM-уровней вместо auto — env сильнее .env
|
||||||
|
|
@ -182,6 +198,72 @@ LEVELS = {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Профиль агента (2026-10-18) ──────────────────────────────────────────────
|
||||||
|
# studio — Коди, агент студии/кооператива (мандаты выше, без изменений).
|
||||||
|
# client — агент клиента Shkor Labs Autopilot на его сервере: своё имя, владелец —
|
||||||
|
# клиент, инструменты тарифа (MCP лабы уровня community). Ставит портал при
|
||||||
|
# развёртывании (scripts/panel_tools.sh пишет SIMBIOT_PROFILE и имена в .env).
|
||||||
|
PROFILE = _env_cfg("SIMBIOT_PROFILE", "studio")
|
||||||
|
AGENT_NAME = _env_cfg("SIMBIOT_AGENT_DISPLAY_NAME", "Коди")
|
||||||
|
OWNER = _env_cfg("SIMBIOT_OWNER_NAME", "Alex")
|
||||||
|
COMPANY = _env_cfg("SIMBIOT_COMPANY", "")
|
||||||
|
|
||||||
|
_CLIENT_COMMON = (
|
||||||
|
"Файлы в чат — только идемпотентно: cli.py tg-deliver-file <путь> -c \"подпись\" "
|
||||||
|
"(повтор — SKIP); прямые curl-отправки из shell запрещены. "
|
||||||
|
"Инструмент portal_mcp — MCP лаборатории Shkor Labs на тарифе Autopilot: "
|
||||||
|
"git_repo_create/git_repo_list/git_repo_info — личные репозитории владельца на "
|
||||||
|
"git.cil.by; site_request — поддомен <slug>.cil.by на этот сервер (до 3); "
|
||||||
|
"project_create — проект владельца (доля студии от 10%, удерживается с каждой "
|
||||||
|
"оплаты); product_create/product_list — продукты с платёжной ссылкой (стартуют в "
|
||||||
|
"тестовом режиме, боевые платежи включает студия после ревью — заявка в кабинете "
|
||||||
|
"shkor.com/account); webhook_route_* — вебхуки оплат своего проекта; earnings — "
|
||||||
|
"заработок; skill_hub_search/skill_get — готовые скиллы; feature_request — "
|
||||||
|
"пожелания студии. Реестров кооператива, звонков, почты, казны и голосований у "
|
||||||
|
"тебя НЕТ — не обещай их. Если просят то, чего ты не умеешь, скажи прямо и "
|
||||||
|
"предложи, что можно сделать. "
|
||||||
|
"Не исследуй исходники харнеса и не разбирай падения тиков сам — эскалируй: "
|
||||||
|
"cli.py escalate \"причина\" -c \"контекст\" -s critical|warn|info.")
|
||||||
|
|
||||||
|
|
||||||
|
def _client_mandates() -> dict:
|
||||||
|
who = f"«{COMPANY}»" if COMPANY else "своего владельца"
|
||||||
|
ident = (f"Ты — {AGENT_NAME}, AI-агент компании {who} на платформе Shkor Labs "
|
||||||
|
f"(владелец — {OWNER}). Ты живёшь на собственном сервере владельца и "
|
||||||
|
f"работаешь на его проект. Говори о себе в первом лице как {AGENT_NAME}; "
|
||||||
|
"ты НЕ Коди и не агент кооператива 42. Рядом в панели <slug>.ui.shkor.com "
|
||||||
|
"владельцу доступны OpenCode и Claude Code — для разработки и правок кода "
|
||||||
|
"предлагай их. ")
|
||||||
|
return {
|
||||||
|
"min": (f"Ты — минутный пульс харнеса Simbiot. {ident}"
|
||||||
|
"Все неотвеченные сообщения владельца приходят тебе ОДНИМ батчем (секция "
|
||||||
|
"входящих) — обработай его целиком за тик: один сводный ответ "
|
||||||
|
"(tg-reply <id1>,<id2>,… -c \"текст\") или отдельные реплаи, но ни одно "
|
||||||
|
"сообщение не должно остаться без ответа. Тон: кратко, тепло, на языке "
|
||||||
|
"владельца. СНАЧАЛА ОТВЕТ, ПОТОМ РАБОТА: если работа дольше ~30 с — первым "
|
||||||
|
"делом короткий tg-reply («понял, делаю: …»), итог — cli.py tg-reply <id> "
|
||||||
|
"--followup \"итог\". Мелкие задачи до 5 минут делай сам; большие подавай "
|
||||||
|
"задачей (cli.py task -a — результат сам придёт в чат) и скажи об этом. "
|
||||||
|
"Не создавай задачи из служебных сообщений («тест», «привет»). "
|
||||||
|
+ _CLIENT_COMMON),
|
||||||
|
"hour": (f"Ты — часовой поток харнеса Simbiot. {ident}"
|
||||||
|
"Дайджест часа: что сделано по проекту владельца, статусы задач, батч "
|
||||||
|
"задач S/M/L строго в порядке очереди. Эскалации нижних уровней (секция "
|
||||||
|
"ЭСКАЛАЦИИ) — твоя зона: разберись и закрой cli.py escalation-ack <id>. "
|
||||||
|
"Выбор из 2–9 вариантов — cli.py decide -q \"вопрос\" -o ключ=описание. "
|
||||||
|
+ _CLIENT_COMMON),
|
||||||
|
"day": (f"Ты — дневной цикл харнеса Simbiot. {ident}"
|
||||||
|
"Манифест дня: план по проекту владельца, выбор L-задач, дневной отчёт "
|
||||||
|
"владельцу — что сделано, что дальше, что нужно от него (продукт, "
|
||||||
|
"трекшн, следующий уровень в кабинете shkor.com/account). "
|
||||||
|
+ _CLIENT_COMMON),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if PROFILE == "client":
|
||||||
|
for _name, _mandate in _client_mandates().items():
|
||||||
|
LEVELS[_name] = replace(LEVELS[_name], mandate=_mandate)
|
||||||
|
|
||||||
# v0.2+: week/month/quarter/year — включаются по мере готовности
|
# v0.2+: week/month/quarter/year — включаются по мере готовности
|
||||||
FUTURE_LEVELS = ("week", "month", "quarter", "year")
|
FUTURE_LEVELS = ("week", "month", "quarter", "year")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ from datetime import datetime, timezone, timedelta
|
||||||
|
|
||||||
from . import es
|
from . import es
|
||||||
from .levels import LEVELS
|
from .levels import LEVELS
|
||||||
|
from . import levels as _levels
|
||||||
from . import paths as _paths
|
from . import paths as _paths
|
||||||
from . import runtime
|
from . import runtime
|
||||||
from . import commands
|
from . import commands
|
||||||
|
|
@ -613,7 +614,7 @@ def build_context(level_cfg, model: dict, batch=None):
|
||||||
own = _own_messages(chat_ids, replied_hist) if depth >= 1 else []
|
own = _own_messages(chat_ids, replied_hist) if depth >= 1 else []
|
||||||
if own:
|
if own:
|
||||||
parts.append("ТВОИ НЕДАВНИЕ СООБЩЕНИЯ В ЭТОТ ЧАТ (сам отправил: итоги задач, "
|
parts.append("ТВОИ НЕДАВНИЕ СООБЩЕНИЯ В ЭТОТ ЧАТ (сам отправил: итоги задач, "
|
||||||
"уведомления, предложения — Alex может отвечать на них):\n"
|
f"уведомления, предложения — {_levels.OWNER} может отвечать на них):\n"
|
||||||
+ "\n".join(own))
|
+ "\n".join(own))
|
||||||
if replied_hist:
|
if replied_hist:
|
||||||
replied_hist = sorted(replied_hist, key=lambda x: x.get("ts") or "")[-12:]
|
replied_hist = sorted(replied_hist, key=lambda x: x.get("ts") or "")[-12:]
|
||||||
|
|
|
||||||
51
tests/test_profile.py
Normal file
51
tests/test_profile.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
"""Профиль агента: studio (Коди) по умолчанию, client — агент клиента Autopilot со своим именем."""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
CODE = ("import json; from harness.levels import LEVELS, PROFILE, OWNER; "
|
||||||
|
"print(json.dumps({'p': PROFILE, 'o': OWNER, 'm': {k: v.mandate for k, v in LEVELS.items()}}))")
|
||||||
|
|
||||||
|
|
||||||
|
def run(env):
|
||||||
|
e = {k: v for k, v in os.environ.items() if not k.startswith("SIMBIOT_")}
|
||||||
|
e.update(env)
|
||||||
|
out = subprocess.run([sys.executable, "-c", CODE], cwd=ROOT, env=e, capture_output=True, text=True, check=True)
|
||||||
|
import json
|
||||||
|
return json.loads(out.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ok = bad = 0
|
||||||
|
|
||||||
|
def eq(c, msg):
|
||||||
|
nonlocal ok, bad
|
||||||
|
if c:
|
||||||
|
ok += 1
|
||||||
|
else:
|
||||||
|
bad += 1
|
||||||
|
print("FAIL", msg)
|
||||||
|
|
||||||
|
st = run({"SIMBIOT_PROFILE": "studio"})
|
||||||
|
eq(st["p"] == "studio", "studio profile")
|
||||||
|
eq("Коди" in st["m"]["min"] and "@juno42bot" in st["m"]["min"], "studio min is Cody")
|
||||||
|
eq("booking_slots" in st["m"]["min"], "studio has booking")
|
||||||
|
|
||||||
|
cl = run({"SIMBIOT_PROFILE": "client", "SIMBIOT_AGENT_DISPLAY_NAME": "Acme Autopilot",
|
||||||
|
"SIMBIOT_OWNER_NAME": "Ann Bee", "SIMBIOT_COMPANY": "Acme"})
|
||||||
|
eq(cl["o"] == "Ann Bee", "owner")
|
||||||
|
for lvl in ("min", "hour", "day"):
|
||||||
|
m = cl["m"][lvl]
|
||||||
|
eq("Acme Autopilot" in m and "«Acme»" in m and "Ann Bee" in m, f"{lvl}: identity")
|
||||||
|
eq("@juno42bot" not in m and "Алекс" not in m and "Alex" not in m, f"{lvl}: no Cody/Alex")
|
||||||
|
eq("booking_slots" not in m and "list_members" not in m and "treasury_status" not in m, f"{lvl}: no studio tools")
|
||||||
|
eq("git_repo_create" in m and "product_create" in m, f"{lvl}: client tools")
|
||||||
|
eq(cl["m"]["sec"] == st["m"]["sec"], "sec level shared")
|
||||||
|
|
||||||
|
print(f"{ok} ok, {bad} fail")
|
||||||
|
sys.exit(1 if bad else 0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Reference in a new issue