Skip to content

Серверний чат партнера — AGNT2-461 ​

Сервер партнера може створити особисту бесіду працівника з Admin Agent магазину за явно наданими правами чату. Той самий ключ може мати права підготовки ресурсів. Самих прав підготовки недостатньо для повідомлень та історії. Вхід через браузер залишається окремим протоколом.

Реалізація та докази ​

Реалізовано в цій гілці; повні результати перевірок записані у звіті AGNT2-461. Ці фікстури не означають злиття, розгортання, випуск бойових ключів або перевірку справжнього Store/OpenCart.

ДоказСтан
Гілка AGNT2-461Реалізовано; повну API-перевірку завершено, обидва збої усунено цільовими повторними перевірками
Базові PR433 #344, 437 #347, 457 #348; це не випуск серверного чату
Злиття/розгортання серверного чатуЦей worker не виконував
Локальний mock MCPПеревірено HTTP-підготовку, повідомлення, повтор, SSE та історію без платного провайдера
Справжні локальні Store/OpenCartКоординатор підтвердив підготовку, грант працівника, catalog_get product9001, завершення, повтор, SSE resume та історію; відповідь моделі синтетична
Live devНе перевірено; потрібні дозвіл оператора та явно виданий server_chat ключ

Кожен зовнішній працівник отримує постійну ідентичність у своєму namespace, без підміни власником команди. Синхронізація вимагає chat:membership; зміна ролі owner/admin/member збільшує membershipVersion. Монотонний sourceRevision відокремлено від subjectVersion/membershipVersion. Нове незмінене підтвердження подовжує оренду максимум на п’ять хвилин; повторні та застарілі підтвердження її не подовжують. Відкликане членство та видалена ідентичність не відновлюються. Перед пониженням останнього власника призначте заміну; відкликання з міркувань безпеки не потребує нового власника.

Спочатку створіть розмову, потім отримайте грант Store для conversationId (ChatThread.id), прив’яжіть його з expectedVersion=0 і надішліть людське повідомлення. Відповідь повертає єдиний дозволений оператором trustedContext MCP server/destination; відсутність або неоднозначність забороняють роботу. Агент лише для розмови — створений із профілем без MCP — MCP не має взагалі: відповідь містить mcp: null, грант Store і прив’язка не потрібні, GET context повідомляє «неактивний», PUT context відхиляється. Зашифрований грант прив’язаний до працівника, організації, середовища, команди, магазину/агента, розмови та адреси. Статичні initialize/tools/list не надають даних магазину. Ідентифікатори заголовків ходу та MCP-виклику створює сервер.

Квитанції ідемпотентності переживають ротацію ключа: повтор повертає попередню розмову/запуск, змінений запит — 409 CHAT_CONFLICT. Паралельні повідомлення конфліктують за активного запуску. Розрив з’єднання не скасовує хід. Невизначений розпочатий запуск стає interrupted і автоматично не повторюється. Історія йде від нових повідомлень до старих, продовження — nextCursor. Хід із done.stopReason: completed отримує completed, навіть після помилок окремих інструментів. Лише завершення через tool_error дає failed / TOOL_REFUSED; max_iterations залишається limited. Результат визначають run_status або polling запуску. Обидва містять toolErrors: [{toolCallId, name, errorCode}]; без помилок це порожній масив. errorCode — безпечний TOOL_FAILED, без сирих аргументів і результатів. Строгий клієнт може вважати будь-який запуск із непорожнім toolErrors невдалим.

Чинні ключ, організація, зовнішня ідентичність, фактичне членство/роль, епоха й оренда перевіряються під час допуску, видачі потоку та зовнішніх дій. Спільні нотатки/курація, пристрої та інструменти керування агентами виключені. Worker доступний лише за власним грантом агента, затвердженим оператором (setupProfile.worker.tools у зафіксованому профілі provisioning); воркер піднімається за повноваженнями цього ходу, які перевіряються знову під час обробки черги, а не за членством у команді, якого зовнішня людина не має; квоти, обрана модель, облік витрат і trusted MCP збережені. Видалення розмови/агента/команди стирає вміст і події запусків, зберігаючи неактивну прив’язку та квитанції. Ширші AGNT2-442/444 не оголошені завершеними.

Помилки: CREDENTIAL_REJECTED 401; CHAT_UNAVAILABLE 404; CHAT_CONFLICT 409; MCP_CONTEXT_CONFLICT 409; CHAT_INVALID 400; CHAT_CONFIGURATION 503; EXECUTION_DENIED і MCP_CONTEXT_DENIED. Сирі помилки провайдера не видаються. Для dev-перевірки потрібні розгорнуті маршрути, профіль оператора та ключ із явно схваленими правами чату; чинні права не розширюються.

HTTP contract ​

Authorization: Bearer <organization key with required chat scope>; base /v1/orgs/:orgId/chat. JSON: {success:true,data:...}. PARTNER_API_ENVIRONMENT=test|production is independent of NODE_ENV.

Method/pathScopeRequestResponse data
PUT /membershipschat:membership{externalUserId, externalTeamId, sourceRevision, role, active, validUntil}{actorId,teamId,sourceRevision,role,subjectVersion,membershipVersion,active,validUntil}
POST /conversationschat:create{externalUserId,externalTeamId,agentId} + Idempotency-Key`{conversationId,actorId,teamId,agentId,mcp:
GET /conversations/:conversationId/contextchat:contextactor query{version,expiresAt,active} (no grant)
PUT /conversations/:conversationId/contextchat:context{externalUserId,externalTeamId,serverId,destination,grant,expiresAt,expectedVersion}{version}
POST /conversations/:conversationId/messageschat:run{externalUserId,externalTeamId,message,timezone?} + Idempotency-Key202 {runId,conversationId,turnId,status,reason,createdAt}
GET /conversations/:conversationId/runs/:runIdchat:readquery externalUserId,externalTeamIdsame run shape
GET /conversations/:conversationId/runs/:runId/eventschat:readquery externalUserId,externalTeamId; Last-Event-ID optionalSSE id=runId:sequence, event=type, data=JSON
GET /conversations/:conversationId/messageschat:readquery externalUserId,externalTeamId,cursor?,limit? (1–200, default 50){items:[{id,turnId,role,text,createdAt,attachments,parts?}],nextCursor}

SSE ​

id: <runId>:<sequence> / event: <type> / data: <JSON> / blank line. Last-Event-ID resumes after the last delivered sequence. Public tool-call IDs are server-minted; grants never enter event payloads.

eventJSON data
turn{type:"turn",turnId:string,ts:ISO8601}
text{type:"text",text:string} (append delta)
tool_call{type:"tool_call",name:string,id:string,detail?:string}
tool_result{type:"tool_result",name:string,ok:boolean,error?:string}; no raw tool arguments/results
done`{type:"done",stopReason:"completed"
run_status`{type:"run_status",status:"completed"
error{type:"error",code:string}; safe execution code (for example CHAT_EXECUTION_FAILED); poll run for status

Ім’я SSE-події завжди збігається з data.type. Якщо хід дійшов до кінця, включно з max_iterations, done надходить перед run_status. thinking навмисно прихований: внутрішні міркування можуть містити чутливий контекст. Необов’язковий detail інструмента пропущено; помилки інструмента містять безпечний код TOOL_FAILED, а не текст провайдера. Журнал розмови зберігає окремий словник turn.started / text.delta / tool.started / tool.finished.

Reproducible local proof ​

sh
node api/scripts/partner-chat-smoke.mjs --check
PARTNER_CHAT_SMOKE_OPT_IN=TEST_ONLY node api/scripts/partner-chat-smoke.mjs --test
# Independent, no paid provider; uses reserved workspace API port + 5 / + 6:
PARTNER_CHAT_SMOKE_OPT_IN=TEST_ONLY node api/scripts/partner-chat-smoke.mjs --fixture-mock
# Actual local Store MCP; private input JSON required:
PARTNER_CHAT_SMOKE_OPT_IN=TEST_ONLY node api/scripts/partner-chat-smoke.mjs --fixture

--test uses workspace TEST_DB_NAME, the real HTTP API, provisioning, orchestrator, model resolution, quotas, usage meter and HTTP MCP. Only completion/embedding providers are replaced. Fixture usage records are synthetic; no paid provider call occurs. Do not run concurrent suites on this database. --fixture* uses a separate _test_integration database and stops after 30 minutes.

Private input: .orca/logs/store-fixture-input.json, mode 0600, {mcpTransportUrl,staticCredential,toolName,toolInput}. Only an explicitly approved read-only tool and exact loopback HTTP/HTTPS endpoint on a registered port are accepted. HTTPS trusts a process-specific NODE_EXTRA_CA_CERTS; never disable certificate validation. The test-only transport maps canonical https://93.184.216.34/mcp to that local endpoint. This does not alter production egress.

Private output: .orca/logs/server-chat-fixture.json, mode 0600, contains fixture apiOrigin, organizationId, namespace, environment, distinct provisioningKey/chatKey, destination, staticCredential, expiresAt. Never paste this file into logs, chat, commits or browser bundles. Mock mode also supplies grantIssuerUrl/grantIssuerKey; POST {conversationId,agentId,destination} using bearer grantIssuerKey returns {grant,expiresAt}. This grant issuer is test-only, not the Store protocol.

Відновлення контексту: якщо відповідь прив’язки втрачено, спочатку отримайте актуальну версію через GET. Відсутня прив’язка повертає {version:0,expiresAt:null,active:false}, прострочена зберігає фактичну версію. Відкликана прив’язка залишається забороненою. Видайте новий грант і виконайте прив’язку з отриманою версією; конфлікт CAS потребує повторного GET. Не вгадуйте версію та не повторюйте прострочений грант.

Для незалежного екземпляра задайте PARTNER_CHAT_FIXTURE_WORKSPACE=/absolute/initialized-worktree. Спочатку зарезервуйте порт API workspace + 5 для API фікстури та + 6 для mock MCP. Використовуються окремі тестова БД, закриті файли й Redis. Перезапуск створює нові ключі та instanceId: перечитайте закритий файл і видаліть прив’язки старого екземпляра. fixtureExpiresAt визначає 30-хвилинний строк.

Час ходу ​

GET …/conversations/:conversationId/runs/:runId повертає timings у мілісекундах. Новий запуск має попередній знімок (complete: false); підсумок зберігається після завершення, помилки або скасування. Після аварійної зупинки знімок може лишитися неповним. Для старих запусків без вимірювань — null. Порівнюйте час після кінцевого стану запуску.

queuedMs — від прийняття до захоплення запуску; admissionMs — допуск, lockMs — блокування, agentMs — підготовка агента, promptMs — контекст і пам’ять, toolSetupMs — виявлення MCP, workerMs — запуск воркера й очікування сесії, classificationMs — вибір рівня моделі, modelMs — модель, toolMs — інструменти без очікування воркера, historyMs — запис історії, otherMs — решта оркестрації й доставка потоку. setupMs — сума admission/agent/prompt/toolSetup/worker/classification.

queuedMs + setupMs + lockMs + modelMs + toolMs + historyMs + otherMs = totalMs з похибкою чисел з рухомою комою. Компоненти setupMs вдруге не додають. Вкладені етапи не перетинаються. Черга вимірюється за годинником, виконання — монотонним таймером. totalMs закінчується після запису історії та звільнення блокування, до остаточної доставки партнерського статусу. Прямий хід без черги має queuedMs = 0.

iterations містить modelMs, firstTokenMs (до першого непорожнього текстового/мисленнєвого фрагмента; null без потоку або фрагмента), toolMs, впорядковані tools: [{ durationMs }] та доступні лічильники usage: inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, cacheWrite1hTokens. wallMs — повний час запиту моделі, включно з доставкою фрагментів; modelMs — лише очікування моделі, доставка входить у otherMs. firstTokenMs — частина wallMs. wallMs до суми етапів не додають. Повторні спроби провайдера входять у модель. Узгодження MCP всередині виклику входить у час інструмента, виявлення — у підготовку.

Один JSON-запис event: "turn_timings", turnId, timings на хід містить ті самі числа. Він не містить повідомлень, промптів, секретів, назв/аргументів/результатів інструментів або тексту помилок. Відсутні лічильники невідомі, а не нульові.

Підсумковий run_status містить той самий знімок timings, що й об’єкт запуску, поруч із toolErrors. Для старих запусків без вимірювань залишається null.

Витрати та вартість ходу ​

GET run і термінальний run_status містять той самий usage:

json
{"model":"claude-sonnet-5-5","inputTokens":9120,"outputTokens":410,"cacheReadTokens":0,"cacheWriteTokens":0,"costUsd":"0.033510000000","priceSide":"cost","kinds":[{"kind":"llm_tokens","costUsd":"0.033510000000"}]}

costUsd — точний десятковий рядок із 12 знаками: сума збережених BillingEvent.costUsdPico цього turnId, поділена на 10^12. Використовуйте decimal, не floating point. priceSide: "cost" означає собівартість Agentfy за UsagePrice.side=cost, а не ціну перепродажу. Кредити партнера й charge поза цим контрактом.

Лічильники підсумовують усі виклики ходу, включно з інструментами, повторами й вибором моделі. cacheWriteTokens об'єднує запис кешу на 5 хвилин і на годину. model — остання потокова модель ходу, інакше остання токенова модель ходу; null, якщо її немає. За кількох моделей models містить для кожної {model,inputTokens,outputTokens,cacheReadTokens,cacheWriteTokens,costUsd}. kinds: [{kind,costUsd}] завжди розбиває загальну вартість за видами, включно з нетокеновими подіями ходу, наприклад runtime_seconds. Сума kinds дорівнює підсумку; вартість models стосується лише токенів.

usage: null повертається для accepted/running, історичних ходів, відсутності записів, хоча б однієї події без ціни, невдалого запису журналу або невизначених витрат провайдера (cancelled/interrupted чи виняток). Failed із повністю записаними відповідями моделі (наприклад відмова інструмента) може мати відомі витрати. Null означає «не враховано», а не «безкоштовно»; частковий підсумок не видається за повний.

Перед звичайним термінальним результатом сервер чекає запису журналу й атомарно зберігає знімок разом із подією. GET і повтор SSE читають той самий знімок, не перераховуючи його після зміни цін або видалення старого журналу. Термінальний null залишається null. Поки хід активний, повторюйте GET до термінального статусу. Витрати без прив'язки до цього turnId (наприклад пізніше завершення сесії воркера) не входять у вартість ходу. Доступ — попередній server_chat і перевірка власника розмови.

Частини ходу в історії ​

GET …/conversations/:conversationId/messages зберігає пагінацію від нових повідомлень до старих і попередній об’єднаний text без змін (поле API має назву text, не content). Повідомлення assistant додатково містять необов’язковий масив parts у порядку журналу:

json
[{"type":"text","text":"Перевіряю магазин."},{"type":"tool_call","toolCallId":"call-1","name":"store_status","status":"succeeded"},{"type":"text","text":"Магазин доступний."}]

Сусідні текстові фрагменти об’єднуються; виклики інструментів розділяють текстові частини. Пробіли й роздільники кроків зберігаються з журналу. Остання text-частина — відповідь, якщо хід дійшов до відповіді; попередні — примітки про перебіг роботи. Перерваний або обмежений хід може містити лише примітки: перевіряйте статус запуску. Окремого дублюючого поля answer немає: журнал залишається єдиним джерелом порядку частин.

Статус інструмента: succeeded, failed або cancelled. Сирі аргументи, результати й внутрішні міркування не видаються. Помилка інструмента сама по собі не означає провал запуску. У старих повідомлень без журналу parts відсутнє: показуйте попередній text, не вгадуючи межі. Поле також відсутнє, доки журнал не містить завершальної події інструмента. У user-повідомлень parts немає.

На сторінку виконується одна вибірка журналу за ходами assistant-повідомлень у межах розмови, незалежно від кількості повідомлень. Розбору об’єднаного тексту й запитів на кожне повідомлення немає. Пагінація й завантаження вкладень не змінюються. Семиденне вікно повтору SSE не обмежує цю проєкцію історії.