diff --git a/artifacts/alex_photo.png b/artifacts/alex_photo.png new file mode 100644 index 0000000..2e05fd3 Binary files /dev/null and b/artifacts/alex_photo.png differ diff --git a/artifacts/antivirus-whitelabel-proposal.html b/artifacts/antivirus-whitelabel-proposal.html new file mode 100644 index 0000000..2498dd5 --- /dev/null +++ b/artifacts/antivirus-whitelabel-proposal.html @@ -0,0 +1,119 @@ + + + + + +Приложение к соглашению — White label партнёрство (антивирусный продукт) + + + +
+ +
+
Приложение к соглашению
+

Партнёрство по модели White Label

+
Антивирусный продукт партнёра, выпускаемый под собственным брендом кооператива.
+ Черновик · на согласование +
+ +
+

1. Предмет

+
+

Мы предлагаем партнёрство по модели white label: мы берём ваш антивирусный продукт, выпускаем его под собственным брендом и выводим на рынок — маркетинг, продажи, поддержка клиентов и операционный слой остаются на нашей стороне. Вы фокусируетесь на продукте и технологических обновлениях.

+
+
+ +
+

2. Роли сторон

+
+
2.1
+

Партнёр (вы)

+
    +
  • предоставляет готовый антивирусный продукт и его обновления;
  • +
  • обеспечивает работоспособность ядра и критических исправлений;
  • +
  • передаёт документацию, необходимую для продвижения и поддержки.
  • +
+
+
+
2.2
+

Мы (кооператив)

+
    +
  • выпускаем продукт под собственным брендом (white label);
  • +
  • берём на себя маркетинг, привлечение клиентов и продажи;
  • +
  • обеспечиваем первую линию поддержки пользователей;
  • +
  • ведём юрлицо, платёжную инфраструктуру и биллинг.
  • +
+
+
+ +
+

3. Экономика

+
+

Выручка от продаж продукта распределяется между сторонами. Конкретная доля каждой стороны, периодичность выплат и порядок учёта фиксируются в основном соглашении. Базовый ориентир для обсуждения — равное участие: продукт с одной стороны, дистрибуция и бренд — с другой.

+
+
+ +
+

4. Права на бренд и продукт

+
+

Интеллектуальная собственность на продукт остаётся за партнёром. Кооперативу передаётся право использования продукта под своим брендом в объёме, необходимом для исполнения настоящего партнёрства. Права на клиентскую базу, маркетинговые материалы и бренд, под которым выпускается продукт, принадлежат кооперативу.

+
+
+ +
+

5. Следующие шаги

+
+
    +
  • согласовать доли и условия основного соглашения;
  • +
  • определить бренд и упаковку продукта под нашим именем;
  • +
  • согласовать план запуска и первые продажи.
  • +
+
+
+ +
+
+
От кооператива
+
+
Подпись / ФИО / дата
+
+
+
От партнёра
+
+
Подпись / ФИО / дата
+
+
+ + + +
+ + diff --git a/artifacts/iishnitsa-vision-session.html b/artifacts/iishnitsa-vision-session.html new file mode 100644 index 0000000..588cb3b --- /dev/null +++ b/artifacts/iishnitsa-vision-session.html @@ -0,0 +1,85 @@ + + + + + +Общее видение — панельная сессия для ИИШНИЦЫ + + + + +

Панельная сессия «Общее видение»

+

Формат: ИИШНИЦА · 90 минут · панель + интерактив из зала.

+ +
+Миссия кооператива 42 как рамка сессии: мы верим, что осмысленное будущее строится вместе — каждый привносит своё видение, а общее оказывается больше суммы личных. Сессия — микромодель кооператива: сначала каждый мечтает в одиночку, потом замечает чужое, затем вместе рождается общее имя будущего. +
+ +

Драматургия: три круга

+
    +
  1. Круг 1 — «Моё будущее» (4 мин/участник): каждый панелист без прерываний рассказывает, каким он видит своё (или общего «нашего») будущее. Без оценок, без споров — просто мечта вслух.
  2. +
  3. Круг 2 — «Что понравилось у других» (2 мин/участник): каждый называет 1–2 образа из чужих рассказов, которые отозвались, и почему. Здесь рождается ткань общего.
  4. +
  5. Круг 3 — «Название общего видения» (10–15 мин): панель + зал вместе придумывают и выбирают название общего видения. Голосование аплодисментами/руками.
  6. +
+ +

Тайминг на 90 минут

+ + + + + + + + + + +
ВремяБлокКто
0:00–0:10Открытие: миссия 42 как рамка, представление панелей, правила сессииМодератор
0:10–0:30Круг 1 «Моё будущее» — 4 мин × 5 панелей (или 5 мин × 4)Панелисты
0:30–0:42Круг 2 «Что понравилось у других» — 2 мин × панелейПанелисты
0:42–0:50Пауза / музыкальная интерлудия, зал пишет карточки с вариантами названийВсе
0:50–1:05Круг 3: сбор вариантов названий, короткая защита 2–3 финалистов, голосованиеПанель + зал
1:05–1:20Объявление названия общего видения + короткий синтез модератораМодератор
1:20–1:30Вопросы из зала (блиц, по 1 мин)Зал
1:30Закрытие: фото с названием, анонс продолженияМодератор
+ +

Роли

+ + + + + + + +
РольЗадачи
МодераторДержит тайминг и рамку миссии 42, режет переспуски, ведёт круг 3 и голосование
Тайм-киперВидимый таймер (4/2 мин), карточки «30 секунд»
Панелисты (4–5)Проходят три круга, готовы к вопросам из зала
Сборщик названийСобирает карточки зала, группирует похожие, выносит финалистов
Фотограф/протоколФото финального названия, короткий конспект для отчёта
+ +

Заготовки названий (если зал молчит — модерируем от них)

+ + +

Чек-лист организатора

+ + +
Правило сессии: в первых двух кругах никто никого не перебивает и не оценивает. Критика допускается только в вопросах из зала — и то бережно.
+ + + diff --git a/artifacts/laws-rb-dao-supplement.html b/artifacts/laws-rb-dao-supplement.html new file mode 100644 index 0000000..4a4da5c --- /dev/null +++ b/artifacts/laws-rb-dao-supplement.html @@ -0,0 +1,166 @@ + + + + + +Законы кооператива для РБ — адаптация DAO Supplement (Вайоминг) под кооператив 42 + + + +
+ +
+
Кооператив 42 · документы · черновик для обсуждения
+

Законы кооператива для Республики Беларусь

+
Адаптация структуры Wyoming DAO Supplement (W.S. 17-31) под производственный кооператив · к задаче t-261014120323 · основа — разбор t-261014111325 · 14.10.2026
+
+ +
+ Статус документа: концепт/черновик для обсуждения с юристами и правлением. Это не юридическое заключение и не проект устава: статьи сопоставлены по смыслу, точные реквизиты и редакции норм должны быть проверены юристом перед любым использованием. Требование «проконсультироваться с юристами» зафиксировано участниками кооператива ранее (голосовая заметка 23.09.2026). +
+ +

0. Идея документа

+

Вайоминг сделал из DAO юридическое лицо: не новую форму, а режим поверх существующей LLC. Для Беларуси тот же ход даёт готовая форма — производственный кооператив (артель): она member-owned, предполагает коллегиальное управление и распределение результата по участию, а «цифровой слой» поверх неё легализован Декретом №8 «О развитии цифровой экономики» (ПВТ): права можно закреплять в токенах, а сделки совершать смарт-контрактами.

+
Формула: кооператив (юрлицо) + токены (учёт прав) + смарт-контракты (исполнение решений) — белорусский эквивалент вайомингской DAO LLC.
+ +

1. Статус юридического лица

+
+

Вайоминг (§104): DAO — это LLC, в уставе которой есть заявление о DAO-статусе и NOTICE OF RESTRICTIONS; в названии — «DAO LLC».WY

+

Адаптация для РБ: регистрируется производственный кооператив в ЕГРЮЛ. В уставе — раздел «Цифровой режим управления»: фиксируется, что (а) права членов учитываются наряду с реестром в распределённом реестре (блокчейне), (б) решения органов оформляются on-chain операциями, (в) действует внутренний протокол «Конституция кооператива» (код + текст), на который устав прямо ссылается.

+ Право РБ: ст. 104–113 ГК РБ (производственный кооператив/артель); устав обязателен, регистрация в ЕГР; фирменное наименование с указанием «производственный кооператив»/«артель». +

риск В РБ нет аналога «DAO-статуса» как регистрационной пометки — цифровой режим существует только внутри устава и локальных актов. Это не блокер: вайомингский статус тоже по сути содержательный, а не регистрационный.

+
+ +

2. Применимое право

+
+

Вайоминг (§103): DAO Supplement + весь LLC Act поверх, кроме противоречий.WY

+

Адаптация для РБ: иерархия источников: (1) ГК РБ и законодательство о юрлицах; (2) закон о регистре/Декрет №8 для цифрового слоя; (3) устав кооператива; (4) внутренние акты — «Конституция» (протокол управления); (5) регламенты органов.

+ Право РБ: Декрет №8 от 21.12.2017 «О развитии цифровой экономики» — правовой режим ПВТ: токены, смарт-контракты, операции с ними; для полного эффекта кооперативу целесообразен статус резидента ПВТ. +
+ +

3. Органы управления и смарт-контракты

+
+

Вайоминг (§106, §109): управляют члены и смарт-контракты; публичный идентификатор контракта обязателен, за 30 дней не дали — роспуск.WY

+

Адаптация для РБ: классическая тройка кооператива сохраняется — общее собрание (высший орган), правление, председатель/директор. Смарт-контракт не заменяет органы, а исполняет их решения: голосования собрания проводятся on-chain, итог автоматически оформляется записью в реестре.

+ + Право РБ: высший орган производственного кооператива — общее собрание; текущее управление — правление/председатель; компетенция и порядок — устав. +
+ +

4. Иерархия норм: контракт vs устав

+
+

Вайоминг (§115): смарт-контракт > устав > operating agreement.WY

+

Ключевое отличие: в РБ так нельзя — юридически устав всегда выше. Решение — однонаправленная синхронизация: устав признаёт записи реестра доказательством состояний (состав членов, голоса, решения), а смарт-контракт выступает «исполнительным механизмом» уставных процедур. Коллизия разрешается в пользу устава и решения собрания; fork контракта возможен только решением собрания.

+ + + + + + +
СлойВайоминг DAOКооператив РБ
Юридическая нормаSupplement + LLC ActГК РБ + Декрет №8
Учредительный документArticles (DAO-статус)Устав кооператива
Operating agreementДа«Конституция» (протокол управления)
Что выше при конфликтеСмарт-контрактУстав и собрание (контракт — исполнение)
+
+ +

5. Обязанности членов (фидуциарный вопрос)

+
+

Вайоминг (§110): fiduciary duties по умолчанию отключены, остаётся implied covenant of good faith.WY

+

Адаптация для РБ: «отключить» обязанности членов уставом нельзя — но кооперативная форма и не предполагает жёстких фидуциарных обязанностей как корпорация. Практически: в уставе фиксируется минимальный стандарт добросовестности (запрет конфликтных сделок без раскрытия, запрет подрыва платформы), а детально регулирует «Конституция».

+

отличие РБ-режим защитнее вайомингского: полный отказ от обязанностей недоступен, зато это снижает репутационные риски при работе с партнёрами (ПВТ, институционалы).

+
+ +

6. Пай, доли и голоса

+
+

Вайоминг (§111): доля пропорциональна digital-взносу, либо 1 член = 1 голос.WY

+

Адаптация для РБ: классика кооператива — 1 член = 1 голос (это и есть «member-weight=1» из вайомингского правила по умолчанию). Паевой взнос учитывается в токенах на адресах членов: токен = инструмент учёта пая и доли в фонде кооператива, не эмиссия «ценной бумаги» в смысле законодательства о ценных бумагах. Трудовое участие (основание распределения прибыли) протоколируется on-chain как вклад.

+ Право РБ: прибыль производственного кооператива распределяется, как правило, в соответствии с трудовым участием; пай и порядок возврата пая — устав. Минимум членов — 3; лимит «нетрудовых» членов устанавливается законодательством. +
+ +

7. Информация и прозрачность

+
+

Вайоминг (§112): если данные в открытом блокчейне — права на отчётность нет.WY

+

Адаптация для РБ: реестр членов, голосования, решения и версии контракта публикуются в открытом распределённом реестре. Отчётность для членов и третьих лиц обеспечивается самим реестром + уставной нормой «запись реестра = доказательство». Личные данные — вне реестра (псевдонимизация адресов), соответствие законодательству о защите персональных данных.

+
+ +

8. Вход и выход

+
+

Вайоминг (§113): только по правилам устава; по умолчанию выход = потеря всех прав.WY

+

Адаптация для РБ: приём — решение собрания (on-line голосование + запись в реестре + юр. действие в ЕГР). Выход — заявление члена, кооператив обязан выплатить паевой взнос и причитающуюся долю в порядке и сроки устава (в цифровом слое — расчёт смарт-контрактом). Ключевое отличие от Вайоминга: полностью «обнулить» выходящего нельзя — пай подлежит возврату; токены членства сжигаются, имущественные права — по уставу.

+

отличие РБ защищает выходящего сильнее вайомингского дефолта. Плюс для доверия партнёров, минус — нужен ликвидностный буфер в уставе (норма о фонде выплаты паёв).

+
+ +

9. Реорганизация и ликвидация

+
+

Вайоминг (§114): роспуск: срок, большинство >50%, событие контракта, год бездействия и т.д.WY

+

Адаптация для РБ: в основу берутся основания ГК РБ (решение собрания, суд, реорганизация); цифровой слой добавляет «on-chain триггеры»: смарт-контракт может инициировать голосование о ликвидации (например, по событию «1 год без операций» — прямой аналог вайомингского dormancy), но юридическую силу имеет только решение собрания и регистратор. После ликвидации — выплата паёв, контракт замораживается (archive-режим).

+
+ +

10. Иностранные члены

+
+

Вайоминг (§116): foreign DAO не сертифицируется — только местное формирование.WY

+

Адаптация для РБ: члены — физлица и юрлица, включая иностранных; ограничений по национальности членов устав не вводит. Кросс-граничный вывод: для работы DAO-кооператива в других юрисдикциях не создаётся «иностранная копия» — заключается договорная модель (дочерние структуры/партнёрства), ядро прав остаётся в РБ-кооперативе. Для внешних DAO-LLC (Вайоминг), желающих войти в экосистему, — членство юрлица через представителя.

+
+ +

11. Сводная таблица соответствия

+ + + + + + + + + + + + +
#Wyoming DAO SupplementЭквивалент РБСтатус
104DAO-статус LLC в уставе«Цифровой режим управления» в уставе кооперативапереносимо
103Supplement + LLC ActГК РБ + Декрет №8 + уставпереносимо
106/109Управление членами и контрактамиСобрание + правление; контракт исполняет решенияпереносимо
115Контракт > уставаУстав выше; реестр = доказательство состоянийразличие
110Fiduciary duties offСтандарт добросовестности в уставеразличие
111Взнос digital assets / 1 член = 1 голос1 член = 1 голос; пай в токенах учётапереносимо
112Открытый блокчейн = отчётностьОткрытый реестр + псевдонимизацияпереносимо
113Выход = потеря правВыход с выплатой пая (обязательна)различие
114Триггеры роспуска контрактомOn-chain триггер → голосование → юр. ликвидацияпереносимо
116Без foreign DAOИностранные члены да; «копий» юрлица нетпереносимо
+ +

12. Что дальше (дорожная карта)

+
    +
  1. 1. Юридическая экспертиза: подтвердить статьи ГК РБ и объём Декрета №8 для выбранной модели; уточнить режим токена учёта пая с юристом ПВТ.
  2. +
  3. 2. Резидентство ПВТ: подать документы — без него цифровой слой (токены, смарт-контракты) не получает льготного режима Декрета №8.
  4. +
  5. 3. Конституция v1: зафиксировать протокол управления (кворумы, роли, обновление контракта) — демо: t-261014121824.
  6. +
  7. 4. Устав: внести разделы «Цифровой режим управления», «Фонд выплаты паёв», «Стандарт добросовестности».
  8. +
  9. 5. Пилот on-chain решений: вести собрания правления через реестр параллельно с бумажным протоколом (dual-running) до полной уверенности.
  10. +
+ + + +
+ + diff --git a/artifacts/pvt-blockchain-constitution-demo.html b/artifacts/pvt-blockchain-constitution-demo.html new file mode 100644 index 0000000..b3bad3e --- /dev/null +++ b/artifacts/pvt-blockchain-constitution-demo.html @@ -0,0 +1,205 @@ + + + + + +Конституция кооператива на блокчейне — интерактивное демо для ПВТ + + + +
+ +
+
Кооператив 42 · Интерактивное демо для ПВТ
+

Конституция кооператива на блокчейне

+
Как устав и законы кооператива исполняются on-chain: голосование → решение → юридическое действие. Смарт-контракт — исполнительный механизм, устав — источник власти (ГК РБ + Декрет №8). Основа контента: док законов t-261014120323.
+
+ +
+
+
+

Реестр членов

+
+
Всего членов:—
+
+
Каждый член — адрес в реестре. 1 член = 1 голос (§111 WY / устав). Кликните — проголосовать от имени члена.
+
+
+
+

Смарт-контракт

+
+
+ Адрес:0x42…C0NF (в уставе) + Версия:v1.0 + Кворум:— + Порог:50%+1 от голосовавших + Upgradable:да, dual-confirm +
+
Публичный адрес контракта фиксируется в уставе (аналог §109 WY: неактуальный идентификатор блокирует on-chain операции).
+
+
+
+ +
+
+

Активное предложение (on-chain governance)

+
+
PROPOSAL-1 · governanceоткрыто
Принять Конституцию кооператива v1: цифровой режим управления, учёт паёв в токенах, решения органов оформляются on-chain.
+
Явка / кворум ⅔
+
+
+ За + Против + Не голосовали + +
+
+ + + + +
+
Выберите члена слева и проголосуйте. Когда явка достигнет кворума и большинство — «за», кнопка «Исполнить решение» активируется: смарт-контракт оформит решение собрания on-chain.
+
+
+ +
+

Журнал реестра (ledger)

+
+
GENESIS · peg#0
Реестр привязан к уставу кооператива (РБ): состав органов, паи, правила — из юрдокумента. Токен = учёт пая, не ценная бумага (Декрет №8).
+
+
+
+
+ +
+

Как решение становится юридическим фактом (5 шагов)

+
+
1
Предложение (proposal)
Правление вносит предложение on-chain. В уставе закреплено: повестка собрания = активные предложения реестра.
+
2
Голосование членов (1 член = 1 голос)
Члены голосуют подписью со своего адреса. Тайна личности — псевдонимизация, вес — по уставу, не по капиталу.
+
3
Кворум + порог
Смарт-контракт считает явку и голоса по уставным параметрам (здесь: кворум ⅔, порог 50%+1). Невозможность «накрутки» — математика реестра.
+
4
Исполнение (execute)
Контракт оформляет решение операцией в реестре; для решений, требующих юрдействия (реестр юрлиц, договоры), формируется пакет для подписи председателем — принцип «контракт исполняет, устав властвует».
+
5
Прозрачность (audit)
Любой партнёр (ПВТ, аудит, член) проверяет историю голосований и решений в открытом реестре — без запроса отчётности (§112 WY).
+
+
+ +
+

Привязка к праву РБ (для юристов ПВТ)

+
+
    +
  • Юрлицо: производственный кооператив (ГК РБ, ст. 104–113) — устав обязателен, собрание — высший орган.
  • +
  • Цифровой слой: Декрет №8 «О развитии цифровой экономики» — токены (учёт пая), смарт-контракты (оформление сделок/решений), резидентство ПВТ.
  • +
  • Иерархия: устав выше контракта — реестр признаётся доказательством состояний, контракт — исполнительный механизм; коллизии решает собрание.
  • +
  • Защита членов: выход — с выплатой пая (обязательна); контрактом это автоматизируется, но не отменяется.
  • +
+
+
+ + + +
+ + + + diff --git a/harness/_llm_worker.py b/harness/_llm_worker.py index ebefd8f..a7f3949 100644 --- a/harness/_llm_worker.py +++ b/harness/_llm_worker.py @@ -117,6 +117,131 @@ def _wire_tool_budget(agent, max_calls: int = 30): return agent +# ── e-261014163808-4da3ec: TTFT-watchdog + ретрай висящего вызова ────────── +# +# Диагноз (13:08 и 16:11 14-10-2026, павшие hour-тики): 9router принимает +# POST и открывает стрим, но НИ ОДНОГО байта контента не приходит — в логе +# роутера «▶ POST ... STREAM» есть, «📊 DONE» после него нет. Клиентский +# timeout=90 и SDK-ретраи не спасали: 4 попытки × тот же висящий апстрим, +# в итоге APITimeoutError уносил ВЕСЬ тик (TaskFailed по батчу, CycleError, +# эскалация). Воркер — subprocess, повторить механизм с уровня нельзя, +# поэтому фикс в самом воркере. +# +# Решение: обёртка стрима модели. Признак живого апстрима — первый +# content-chunk; пока его нет, watchdog: N секунд без токена → стрим +# закрываем и делаем СВЕЖИЙ вызов (новый POST — роутер сам уводит его на +# живой апстрим). Начавший отвечать апстрим не трогаем: обрыв на середине +# ответа дороже редкого зависания. +# +# Гарантии: strands дождётся события message_stop новой попытки — цикл +# агента консистентен; BudgetSpent идемпотентен (usage по фактически +# дошедшим assistant-сообщениям); инструменты исполняются после конца +# стрима, так что дублей tool-вызовов нет. + +_TTFT_FIRST_TOKEN_BUDGET = float(os.environ.get("SIMBIOT_LLM_TTFT_BUDGET", "45")) + + +async def _aclose_gen(gen): + """Мягко закрыть асинхронный генератор (отмена висящего стрима).""" + close = getattr(gen, "aclose", None) + if close is not None: + try: + await close() + except Exception: + pass + + +def _chunk_type(chunk) -> str: + """Тип чанка стрима strands: атрибут или ключ словаря.""" + ct = getattr(chunk, "chunk_type", None) + if not ct and isinstance(chunk, dict): + ct = chunk.get("chunk_type") + return ct or "" + + +def _with_ttft_retry(model, level: str, call_timeout: float): + """Обернуть модель: watchdog первого токена + ретраи висящих вызовов. + + first_budget — сколько ждать ПЕРВЫЙ content-chunk; capped сверху + клиентским call_timeout (старый таймаут продолжает страховать весь + вызов на случай совсем глухого апстрима). Попыток 3: нормальный + TTFT роутера 2-13с (лог), 45с — запас x3; 3×45 + ретраи SDK не + пробивают tick_timeout уровня (min 480с, hour 2880с). + """ + import asyncio + import time as _time + + first_budget = min(_TTFT_FIRST_TOKEN_BUDGET, max(float(call_timeout), 1.0)) + max_attempts = 3 + base_stream = model.stream + + def _note_retry(attempt: int, why: str): + # след в стор: сколько раз сработал watchdog (видно из трейса тика) + try: + from harness import es + es.append_event("trace", f"ttft-retry:{level}", "TTFTWatchdogRetry", { + "level": level, "attempt": attempt, "reason": why, + "first_token_budget_s": first_budget, + }) + except Exception: + pass # телеметрия — best effort + + class _TTFTModel: + """Прозрачный прокси: всё делегируется модели, stream() — под watchdog. + + Не наследуем Model (ABC с abstractmethods): strands_harness.resolve_model + принимает по isinstance(model, (Model, ModelRouter)), поэтому после + делегирования регистрируемся как Model через Model.register(_TTFTModel) + — и делегирование __getattr__, и валидный isinstance для create_harness. + """ + + def __init__(self, inner): + object.__setattr__(self, "_inner", inner) + + def __getattr__(self, name): + return getattr(object.__getattribute__(self, "_inner"), name) + + async def stream(self, *args, **kwargs): + attempts = 0 + while True: + attempts += 1 + gen = base_stream(*args, **kwargs) + it = gen.__aiter__() + got_content = False + try: + while True: + try: + chunk = await asyncio.wait_for( + it.__anext__(), timeout=first_budget) + except StopAsyncIteration: + # пустой стрим без контента — тоже симптом висения, + # но ретраить бессмысленно: считаем ответом и уходим + return + ct = _chunk_type(chunk) + if ct in ("content_delta", "content_start"): + got_content = True + yield chunk + if got_content: + break + except asyncio.TimeoutError: + why = f"{first_budget:.0f}s без первого токена (попытка {attempts})" + if attempts < max_attempts: + _note_retry(attempts, why) + await _aclose_gen(gen) + continue # свежий POST: роутер уведёт на живой апстрим + raise TimeoutError( + f"апстрим завис до первого токена: {max_attempts} попытки " + f"по {first_budget:.0f}s — {why}") from None + # апстрим живой (контент пошёл) — доедаем без watchdog + async for chunk in it: + yield chunk + return + + from strands.models.model import Model as _StrandsModel + _StrandsModel.register(_TTFTModel) + return _TTFTModel(model) + + def build_level_model_from_payload(payload: dict): """Модель для воркера — та же сборка, что и в runtime.build_level_model: 9router понимает chat/completions (usage там же), Responses API — шим без usage. @@ -144,7 +269,7 @@ def build_level_model_from_payload(payload: dict): # «Connection error» к роутеру ронял весь тик с первой попытки; openai SDK # умеет сам ретраить connection-error/429/5xx с экспонентой — даём запас. max_retries = int(os.environ.get("SIMBIOT_LLM_MAX_RETRIES", "3")) - return OpenAIModel( + base = OpenAIModel( # timeout открытого клиента (openai SDK) режет каждый chat.completions # вызов; при зависании апстрима получаем APITimeoutError за N сек и # strands event-loop завершает тик штатно (без потери предыдущих шагов) @@ -154,6 +279,11 @@ def build_level_model_from_payload(payload: dict): model_id=model_id, **({"params": params} if params else {}), ) + # e-261014163808-4da3ec (часовой фикс): ретрай висящего ДО-ПЕРВОГО-ТОКЕНА + # апстрим-вызова. Кейс дня (router-лог 16:09:50): POST принят, TTFT так и + # не пришёл, ~90с спустя APITimeoutError унёс ВЕСЬ тик вместе с claimed- + # батчем. SDK-ретраи не спасали: 4 попытки × 90с висения = тик мёртв. + return _with_ttft_retry(base, payload.get("level") or "hour", call_timeout) def _emit(payload, obj): diff --git a/tasks.db b/tasks.db new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_ttft_retry.py b/tests/test_ttft_retry.py new file mode 100644 index 0000000..88e460d --- /dev/null +++ b/tests/test_ttft_retry.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +"""E2E-проверка TTFT-watchdog обёртки воркера (e-261014163808-4da3ec). + +Три сценария на фейковом стриме: + 1. живой апстрим (контент сразу) → все чанки, ни одного ретрая; + 2. висит до первого токена, оживёт на 2-й попытке → ретрай, ответ完整; + 3. висит всегда (3 попытки) → TimeoutError с внятным текстом. + +Плюс дым: сборка модели payload-ом возвращает прокси с рабочим stream(). +Запуск: ./venv/bin/python tests/test_ttft_retry.py +""" +import asyncio +import os +import sys +import types + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from harness import _llm_worker as W # noqa: E402 + +FAILS = [] + + +def check(name, cond, extra=""): + print(("PASS " if cond else "FAIL ") + name + (f" | {extra}" if extra and not cond else "")) + if not cond: + FAILS.append(name) + + +def chunk(ct): + return {"chunk_type": ct} + + +def fake_stream_model(script): + """script: список «попыток», каждая — список чанков или raise Hang.""" + calls = {"n": 0} + + class M: + async def stream(self, *a, **k): + i = calls["n"] + calls["n"] += 1 + act = script[i] + if act == "HANG": + await asyncio.sleep(30) # дольше watchdog-бюджета + yield chunk("message_start") + return + for c in act: + yield chunk(c) + + return M(), calls + + +async def run(model): + wrapped = W._with_ttft_retry(model, "hour", 90) + out = [] + async for c in wrapped.stream(): + out.append(W._chunk_type(c)) + return out + + +async def main(): + os.environ["SIMBIOT_LLM_TTFT_BUDGET"] = "0.5" # быстрый watchdog для теста + W._TTFT_FIRST_TOKEN_BUDGET = 0.5 + + # 1. живой апстрим + m, calls = fake_stream_model([ + ["message_start", "content_delta", "content_delta", "message_stop"]]) + out = await run(m) + check("живой стрим проходит целиком", + out == ["message_start", "content_delta", "content_delta", "message_stop"], + str(out)) + check("живой стрим — 1 попытка", calls["n"] == 1, str(calls)) + + # 2. виснет, оживает на 2-й + m, calls = fake_stream_model([ + "HANG", + ["message_start", "content_delta", "message_stop"]]) + out = await run(m) + check("зависший вызов ретраится и отвечает", + out == ["message_start", "content_delta", "message_stop"], str(out)) + check("ретрай сделал ровно 2 попытки", calls["n"] == 2, str(calls)) + + # 3. виснет всегда + m, calls = fake_stream_model(["HANG", "HANG", "HANG"]) + try: + await run(m) + check("глухой апстрим даёт TimeoutError", False, "не упал") + except TimeoutError as e: + check("глухой апстрим даёт TimeoutError", "завис до первого токена" in str(e), str(e)) + check("глухой апстрим — ровно 3 попытки", calls["n"] == 3, str(calls)) + + # 4. делегирование атрибутов прокси + m, _ = fake_stream_model([["content_delta"]]) + m.foo = "bar" + wrapped = W._with_ttft_retry(m, "hour", 90) + check("прокси делегирует атрибуты", getattr(wrapped, "foo", None) == "bar") + + print() + print("ИТОГ:", "ВСЕ ОК" if not FAILS else f"ПРОВАЛЫ: {FAILS}") + sys.exit(1 if FAILS else 0) + + +asyncio.run(main())