Merge origin/dev: لایهٔ realtime (WebSocket/RPC) + فلو دعوت عضو
ادغام ۱۲ کامیت ریموت (لایهٔ realtime روی WebSocket، حذف REST عمومی، رفع
باگهای صفحهٔ مدلها) با ۸ کامیت محلی (ارائهدهندهٔ agy + رفع فلو دعوت).
تنها کانفلیکت واقعی در `app/main.py` بود و ماهوی بود، نه متنی:
ریموت اپ عمومی را به «فقط /health + /api/ws» کاهش داده و همهٔ هندلرهای HTTP
را به اپ داخلی `interfaces/realtime/rpc_app.py` منتقل کرده است. بنابراین
روتر `invites` من نباید در اپ عمومی mount شود.
راهحل:
- `app/main.py`: فقط `health` و `realtime_router` میمانند؛ ایمپورتهای اضافی
حذف شدند (نسخهٔ ریموت).
- `interfaces/realtime/rpc_app.py`: روتر `invites` به اپ داخلی اضافه شد تا
`GET /api/invites/{token}` و `POST /api/invites/accept` از مسیر WS-RPC
قابل دسترس باشند.
`api.ts` و `contracts/index.ts` خودکار و بدون کانفلیکت ادغام شدند: کلاینت
`previewInvite`/`acceptInvite` حالا از `getRealtimeClient().rpc()` رد میشود،
پس هیچ تغییری در فراخوانیها لازم نبود.
رفع تست شکستهٔ ریموت: `src/test/version.test.tsx` مقدار `AppShell` را بدون
`RealtimeProvider` رندر میکرد و `useRealtime` استثنا میداد. حالا اسکلت
همانطور که در `app/layout.tsx` سیمکشی شده رندر میشود و سوکت در تست stub
شده است.
اعتبارسنجی روی درخت ادغامشده:
- بکاند ۲۷۰ تست سبز (شامل ۶ تست دعوت روی معماری جدید)؛ mypy تمیز.
- فرانت ۶۱ تست سبز؛ tsc و eslint تمیز.
- تأیید شد روترهای invite روی اپ داخلی RPC حاضرند و سطح عمومی تغییر نکرده.
نکته: ۵ خطای ruff در `tests/conftest.py`، `tests/test_error_log.py` و
`tests/test_realtime.py` از خود `origin/dev` میآیند و به این ادغام ربطی
ندارند؛ دستنخورده رها شدند.
This commit is contained in:
@@ -40,11 +40,14 @@ const EFFORT_LABELS: Record<ReasoningEffort, string> = {
|
||||
* model cannot think — the backend rejects a thinking config everywhere else.
|
||||
* Same marker rule as the agy adapter: thinking-capable ids contain one of
|
||||
* `thinking` / `gemini-3` / `gemini-2.5-pro` / `gemini-2.5-flash`, image
|
||||
* models are always excluded.
|
||||
* and claude models are excluded from explicit effort selection.
|
||||
*/
|
||||
export function effortsForModel(modelId: string): ReasoningEffort[] {
|
||||
const lower = modelId.toLowerCase();
|
||||
if (lower.includes("image")) return [];
|
||||
if (lower.includes("claude")) return [];
|
||||
if (lower.includes("gpt")) return ["medium"];
|
||||
if (lower.includes("gemini-3.1-pro")) return ["low", "high"];
|
||||
const canThink =
|
||||
lower.includes("thinking") ||
|
||||
lower.includes("gemini-3") ||
|
||||
@@ -57,6 +60,16 @@ export function effortLabel(effort: string): string {
|
||||
return EFFORT_LABELS[effort as ReasoningEffort] ?? effort;
|
||||
}
|
||||
|
||||
/** A live model reported by an agy account's catalogue. */
|
||||
export interface AgyAccountModel {
|
||||
id: string;
|
||||
display_name: string;
|
||||
supports_thinking: boolean;
|
||||
supported_efforts: ReasoningEffort[];
|
||||
default_effort?: ReasoningEffort | null;
|
||||
supports_images?: boolean;
|
||||
}
|
||||
|
||||
/** Rotation-pool lifecycle of one agy account. */
|
||||
export type AgyAccountStatus = "active" | "cooldown" | "error";
|
||||
|
||||
@@ -670,3 +683,99 @@ export interface ErrorQuery {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Realtime (WebSocket)
|
||||
//
|
||||
// The live feed is the same product surface as the REST API: one envelope
|
||||
// shape, mirrored key-for-key from ``app/application/ports/realtime.py``.
|
||||
// A page that switches on ``WsEventType`` never needs to know which part of
|
||||
// the backend produced the event.
|
||||
//
|
||||
// Channel naming is what a page subscribes to — ``org:{id}`` for the tenant
|
||||
// feed, ``task:{id}`` for one task, and so on. The server authorizes every
|
||||
// subscribe against the caller's memberships, so a channel the user may not
|
||||
// see simply comes back rejected.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type WsEventType =
|
||||
| "run.step"
|
||||
| "run.status"
|
||||
| "task.status"
|
||||
| "task.created"
|
||||
| "chat.pending"
|
||||
| "message.created"
|
||||
| "approval.pending"
|
||||
| "artifact.created"
|
||||
| "sandbox.command"
|
||||
| "sandbox.session"
|
||||
| "sandbox.reaped"
|
||||
| "usage.recorded"
|
||||
| "billing.wallet"
|
||||
| "error.recorded"
|
||||
| "audit.recorded"
|
||||
| "worker.status";
|
||||
|
||||
/** One pushed event, as the browser receives it. */
|
||||
export interface WsEvent<T = Record<string, unknown>> {
|
||||
id: string;
|
||||
type: WsEventType;
|
||||
data: T;
|
||||
organization_id: string | null;
|
||||
/** ISO-8601 UTC; render with ``toLocaleString`` like every other timestamp. */
|
||||
at: string;
|
||||
}
|
||||
|
||||
/** Every frame the server sends. Control frames and events share ``type``. */
|
||||
export type WsServerFrame =
|
||||
| { type: "hello"; user_id: string | null; server_time: string }
|
||||
| { type: "subscribed"; accepted: string[]; rejected: string[] }
|
||||
| { type: "unsubscribed"; channels: string[] }
|
||||
| { type: "pong" }
|
||||
| { type: "error"; message: string }
|
||||
| { type: "event"; event: WsEvent }
|
||||
/** Reply to a client ``rpc`` frame: the HTTP status and parsed JSON body of the
|
||||
* internal dispatch, correlating on ``id``. */
|
||||
| { type: "rpc_result"; id: string; status: number; body: unknown };
|
||||
|
||||
/** Every frame the client may send. */
|
||||
export type WsClientFrame =
|
||||
| { action: "subscribe"; channels: string[] }
|
||||
| { action: "unsubscribe"; channels: string[] }
|
||||
| { action: "ping" }
|
||||
/** A request/response call into the server. ``id`` correlates the
|
||||
* ``rpc_result``; ``path`` is the full mounted path including any query
|
||||
* string. Every application call travels over this frame. */
|
||||
| {
|
||||
action: "rpc";
|
||||
id: string;
|
||||
method: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
||||
path: string;
|
||||
body: unknown;
|
||||
};
|
||||
|
||||
/** Live run-step payload — what the task timeline renders as it arrives. */
|
||||
export interface WsRunStepData {
|
||||
run_id: string;
|
||||
task_id: string;
|
||||
step: {
|
||||
id: string;
|
||||
index: number;
|
||||
kind: string;
|
||||
title: string;
|
||||
payload: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
/** Live sandbox command payload — the terminal panel's stream. */
|
||||
export interface WsSandboxCommandData {
|
||||
session_id: string;
|
||||
run_id: string;
|
||||
command: string;
|
||||
status: string;
|
||||
exit_code: number | null;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
duration_ms: number;
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user