mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(gateway): generate shared TypeScript contracts
Define gateway payloads as Python TypedDict contracts and generate the shared TypeScript surface through ts-type during shared, TUI, and desktop workflows.
This commit is contained in:
parent
75afaf46da
commit
2cdfe35429
12 changed files with 166 additions and 49 deletions
11
.github/workflows/js-tests.yml
vendored
11
.github/workflows/js-tests.yml
vendored
|
|
@ -44,6 +44,17 @@ jobs:
|
|||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # 8.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
cache-dependency-glob: |
|
||||
pyproject.toml
|
||||
uv.lock
|
||||
- name: Install Python development dependencies
|
||||
uses: ./.github/actions/retry
|
||||
with:
|
||||
command: uv sync --locked --extra dev
|
||||
- uses: ./.github/actions/retry
|
||||
with:
|
||||
command: npm ci
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -90,6 +90,7 @@ apps/desktop/src/**/*.d.ts
|
|||
apps/shared/src/**/*.js
|
||||
apps/shared/src/**/*.js.map
|
||||
apps/shared/src/**/*.d.ts
|
||||
apps/shared/src/generated/
|
||||
apps/desktop/release/
|
||||
*.tsbuildinfo
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"predev": "npm run generate:gateway-types --prefix ../shared",
|
||||
"clean": "npm run clean:e2e && npm run clean:renderer && npm run clean:electron",
|
||||
"clean:e2e":"tsc --build tsconfig.e2e.json --clean",
|
||||
"clean:renderer":"tsc --build tsconfig.json --clean ",
|
||||
|
|
@ -23,7 +24,7 @@
|
|||
"profile:main": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .",
|
||||
"profile:main:cpu": "tsc --build tsconfig.electron.json && wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .",
|
||||
"start": "npm run build && electron .",
|
||||
"prebuild": "npm run clean",
|
||||
"prebuild": "npm run generate:gateway-types --prefix ../shared && npm run clean",
|
||||
"build": "node scripts/assert-root-install.mjs && node scripts/write-build-stamp.mjs && vite build && node scripts/bundle-electron-main.mjs && node scripts/stage-native-deps.mjs",
|
||||
"postbuild": "node scripts/assert-dist-built.mjs",
|
||||
"prebuilder": "node scripts/patch-electron-builder-mac-binary.mjs",
|
||||
|
|
@ -45,7 +46,7 @@
|
|||
"test:desktop:nsis": "node scripts/test-desktop.mjs nsis",
|
||||
"test:desktop:existing": "node scripts/test-desktop.mjs existing",
|
||||
"test:desktop:fresh": "node scripts/test-desktop.mjs fresh",
|
||||
"typecheck": "tsc -p . --noEmit && tsc -p tsconfig.electron.json --noEmit && tsc -p tsconfig.e2e.json --noEmit",
|
||||
"typecheck": "npm run generate:gateway-types --prefix ../shared && tsc -p . --noEmit && tsc -p tsconfig.electron.json --noEmit && tsc -p tsconfig.e2e.json --noEmit",
|
||||
"lint": "eslint src/ electron/",
|
||||
"lint:fix": "eslint src/ electron/ --fix",
|
||||
"fmt": "prettier --write 'src/**/*.{ts,tsx}' 'electron/**/*.ts' 'vite.config.ts'",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { GatewaySessionRuntimeInfo } from '@hermes/shared/gateway-contracts'
|
||||
|
||||
export interface ConfigFieldSchema {
|
||||
category?: string
|
||||
description?: string
|
||||
|
|
@ -470,27 +472,7 @@ export interface SessionResumeResponse {
|
|||
status?: string
|
||||
}
|
||||
|
||||
export interface SessionRuntimeInfo {
|
||||
approval_mode?: 'manual' | 'off' | 'smart'
|
||||
branch?: string
|
||||
config_warning?: string
|
||||
credential_warning?: string
|
||||
cwd?: string
|
||||
desktop_contract?: number
|
||||
fast?: boolean
|
||||
install_warning?: string
|
||||
model?: string
|
||||
personality?: string
|
||||
provider?: string
|
||||
reasoning_effort?: string
|
||||
running?: boolean
|
||||
service_tier?: string
|
||||
skills?: Record<string, string[]> | string[]
|
||||
tools?: Record<string, string[]>
|
||||
usage?: Partial<UsageStats>
|
||||
version?: string
|
||||
yolo?: boolean
|
||||
}
|
||||
export type SessionRuntimeInfo = GatewaySessionRuntimeInfo
|
||||
|
||||
export interface UsageStats {
|
||||
calls: number
|
||||
|
|
|
|||
|
|
@ -8,14 +8,16 @@
|
|||
"./billing": "./src/billing-types.ts",
|
||||
"./billing-policy": "./src/billing-policy.ts",
|
||||
"./charge-settlement": "./src/charge-settlement.ts",
|
||||
"./gateway-contracts": "./src/generated/gateway.ts",
|
||||
"./skin": "./src/skin.ts"
|
||||
},
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"generate:gateway-types": "uv run --locked --extra dev python ../../scripts/generate_gateway_types.py",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint src/ --fix",
|
||||
"fix": "npm run lint:fix",
|
||||
"typecheck": "tsc -p . --noEmit",
|
||||
"typecheck": "npm run generate:gateway-types && tsc -p . --noEmit",
|
||||
"check": "npm run typecheck"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ edge-tts = ["edge-tts==7.2.7"]
|
|||
modal = ["modal==1.3.4"]
|
||||
daytona = ["daytona==0.155.0"]
|
||||
hindsight = ["hindsight-client==0.6.1"]
|
||||
dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "mcp==1.26.0", "starlette==1.0.1", "ty==0.0.21", "ruff==0.15.10", "setuptools==81.0.0"] # starlette: CVE-2026-48710; setuptools: latest <82 (torch >=2.11 caps setuptools<82)
|
||||
dev = ["debugpy==1.8.20", "pytest==9.0.2", "pytest-asyncio==1.3.0", "mcp==1.26.0", "starlette==1.0.1", "ts-type==0.3.4", "ty==0.0.21", "ruff==0.15.10", "setuptools==81.0.0"] # starlette: CVE-2026-48710; setuptools: latest <82 (torch >=2.11 caps setuptools<82)
|
||||
messaging = ["python-telegram-bot[webhooks]==22.6", "discord.py[voice]==2.7.1", "aiohttp==3.14.1", "brotlicffi==1.2.0.1", "slack-bolt==1.29.0", "slack-sdk==3.43.0", "qrcode==7.4.2"] # aiohttp 3.14.1: CVE-2026-34513/34518/34519/34520/34525 + 34993(RCE)/47265
|
||||
cron = [] # croniter is now a core dependency; this extra kept for back-compat
|
||||
slack = ["slack-bolt==1.29.0", "slack-sdk==3.43.0", "aiohttp==3.14.1"]
|
||||
|
|
|
|||
45
scripts/generate_gateway_types.py
Normal file
45
scripts/generate_gateway_types.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate TypeScript gateway contracts from ``tui_gateway.contracts``."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
import ts_type as ts
|
||||
from tui_gateway.contracts import (
|
||||
GatewayMcpServerStatus,
|
||||
GatewayProjectInfo,
|
||||
GatewaySessionRuntimeInfo,
|
||||
GatewayUsage,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUTPUT = ROOT / "apps/shared/src/generated/gateway.ts"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for contract in (
|
||||
GatewayMcpServerStatus,
|
||||
GatewayProjectInfo,
|
||||
GatewayUsage,
|
||||
GatewaySessionRuntimeInfo,
|
||||
):
|
||||
ts.generator.add(contract, "gateway", contract.__name__)
|
||||
|
||||
source = ts.generator.render()["gateway"]
|
||||
output = "// Generated by scripts/generate_gateway_types.py. Do not edit.\n\n" + source + "\n"
|
||||
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Workspace checks may run concurrently. Replacing an already-complete
|
||||
# temporary file means a typecheck always observes either version, never a
|
||||
# partially-written module.
|
||||
with NamedTemporaryFile("w", encoding="utf-8", dir=OUTPUT.parent, delete=False) as file:
|
||||
file.write(output)
|
||||
temp_path = Path(file.name)
|
||||
os.replace(temp_path, OUTPUT)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
72
tui_gateway/contracts.py
Normal file
72
tui_gateway/contracts.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""Type-only JSON contracts shared by the TUI gateway and TypeScript clients.
|
||||
|
||||
These ``TypedDict`` definitions describe the wire payloads without changing the
|
||||
runtime's deliberately lightweight plain-dict serialization. ``ts-type``
|
||||
generates the matching TypeScript definitions during frontend checks.
|
||||
"""
|
||||
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
|
||||
class GatewayMcpServerStatusBase(TypedDict):
|
||||
connected: bool
|
||||
name: str
|
||||
tools: int
|
||||
transport: str
|
||||
|
||||
|
||||
class GatewayMcpServerStatus(GatewayMcpServerStatusBase, total=False):
|
||||
disabled: bool
|
||||
status: str
|
||||
|
||||
|
||||
class GatewayProjectInfoBase(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
slug: str
|
||||
|
||||
|
||||
class GatewayProjectInfo(GatewayProjectInfoBase, total=False):
|
||||
primary_path: str | None
|
||||
|
||||
|
||||
class GatewayUsage(TypedDict, total=False):
|
||||
calls: int
|
||||
context_max: int
|
||||
context_percent: int
|
||||
context_used: int
|
||||
cost_usd: float
|
||||
input: int
|
||||
output: int
|
||||
total: int
|
||||
|
||||
|
||||
class GatewaySessionRuntimeInfo(TypedDict, total=False):
|
||||
approval_mode: Literal["manual", "off", "smart"]
|
||||
branch: str
|
||||
config_warning: str
|
||||
credential_warning: str
|
||||
cwd: str
|
||||
desktop_contract: int
|
||||
fast: bool
|
||||
install_warning: str
|
||||
mcp_servers: list[GatewayMcpServerStatus]
|
||||
model: str
|
||||
personality: str
|
||||
profile_name: str
|
||||
project: GatewayProjectInfo | None
|
||||
provider: str
|
||||
reasoning_effort: str
|
||||
release_date: str
|
||||
running: bool
|
||||
service_tier: str
|
||||
skills: dict[str, list[str]] | list[str]
|
||||
stored_session_id: str
|
||||
system_prompt: str
|
||||
title: str
|
||||
tools: dict[str, list[str]]
|
||||
update_behind: int | None
|
||||
update_command: str
|
||||
usage: GatewayUsage
|
||||
version: str
|
||||
yolo: bool
|
||||
|
|
@ -14,9 +14,10 @@ import sys
|
|||
import threading
|
||||
import time
|
||||
import uuid
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, NamedTuple, Optional
|
||||
from typing import Any, NamedTuple, Optional, cast
|
||||
|
||||
from hermes_constants import (
|
||||
get_hermes_home,
|
||||
|
|
@ -29,6 +30,7 @@ from utils import is_truthy_value
|
|||
from tools.environments.local import hermes_subprocess_env
|
||||
from agent.replay_cleanup import sanitize_replay_history
|
||||
from tui_gateway import git_probe
|
||||
from tui_gateway.contracts import GatewaySessionRuntimeInfo
|
||||
from tui_gateway.transport import (
|
||||
StdioTransport,
|
||||
Transport,
|
||||
|
|
@ -1212,14 +1214,14 @@ def write_json(obj: dict) -> bool:
|
|||
return (current_transport() or _stdio_transport).write(obj)
|
||||
|
||||
|
||||
def _event_frame(event: str, sid: str, payload: dict | None = None) -> dict:
|
||||
def _event_frame(event: str, sid: str, payload: Mapping[str, Any] | None = None) -> dict:
|
||||
params: dict = {"type": event, "session_id": sid}
|
||||
if payload is not None:
|
||||
params["payload"] = payload
|
||||
return {"jsonrpc": "2.0", "method": "event", "params": params}
|
||||
|
||||
|
||||
def _emit(event: str, sid: str, payload: dict | None = None):
|
||||
def _emit(event: str, sid: str, payload: Mapping[str, Any] | None = None):
|
||||
write_json(_event_frame(event, sid, payload))
|
||||
|
||||
|
||||
|
|
@ -1245,7 +1247,7 @@ def unregister_live_transport(transport: Transport | None) -> None:
|
|||
_live_transports.discard(transport)
|
||||
|
||||
|
||||
def _broadcast_global_event(event: str, payload: dict | None = None) -> None:
|
||||
def _broadcast_global_event(event: str, payload: Mapping[str, Any] | None = None) -> None:
|
||||
"""Fan a session-less, surface-global event (``skin.changed``) to every
|
||||
connected client. Emitters like the skin watcher run on background threads
|
||||
where ``write_json``'s ladder bottoms out at stdio and WS peers never see
|
||||
|
|
@ -4042,7 +4044,7 @@ def _project_info_for_cwd(cwd: str) -> dict | None:
|
|||
return None
|
||||
|
||||
|
||||
def _session_info(agent, session: dict | None = None) -> dict:
|
||||
def _session_info(agent, session: dict | None = None) -> GatewaySessionRuntimeInfo:
|
||||
if session is None:
|
||||
for candidate in _sessions.values():
|
||||
if candidate.get("agent") is agent:
|
||||
|
|
@ -4161,7 +4163,7 @@ def _session_info(agent, session: dict | None = None) -> dict:
|
|||
warn = _probe_credentials(agent)
|
||||
if warn:
|
||||
info["credential_warning"] = warn
|
||||
return info
|
||||
return cast(GatewaySessionRuntimeInfo, info)
|
||||
|
||||
|
||||
def _tool_ctx(name: str, args: dict) -> str:
|
||||
|
|
|
|||
|
|
@ -4,12 +4,13 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"predev": "npm run generate:gateway-types --prefix ../apps/shared",
|
||||
"dev": "npm run build:ink && tsx --watch src/entry.tsx",
|
||||
"start": "tsx src/entry.tsx",
|
||||
"build": "node scripts/build.mjs",
|
||||
"start": "npm run generate:gateway-types --prefix ../apps/shared && tsx src/entry.tsx",
|
||||
"build": "npm run generate:gateway-types --prefix ../apps/shared && node scripts/build.mjs",
|
||||
"build:ink": "npm run build --prefix packages/hermes-ink",
|
||||
"visual": "node scripts/visual/run.mjs",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||
"typecheck": "npm run generate:gateway-types --prefix ../apps/shared && tsc --noEmit -p tsconfig.json",
|
||||
"lint": "eslint src/ packages/",
|
||||
"lint:fix": "eslint src/ packages/ --fix",
|
||||
"fmt": "prettier --write 'src/**/*.{ts,tsx}' 'packages/**/*.{ts,tsx}'",
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import type { GatewayMcpServerStatus, GatewayProjectInfo } from '@hermes/shared/gateway-contracts'
|
||||
|
||||
export interface ActiveTool {
|
||||
context?: string
|
||||
id: string
|
||||
|
|
@ -140,21 +142,8 @@ export type ThinkingMode = 'collapsed' | 'truncated' | 'full'
|
|||
export type SectionName = 'thinking' | 'tools' | 'subagents' | 'activity'
|
||||
export type SectionVisibility = Partial<Record<SectionName, DetailsMode>>
|
||||
|
||||
export interface McpServerStatus {
|
||||
connected: boolean
|
||||
disabled?: boolean
|
||||
status?: 'configured' | 'connecting' | 'connected' | 'disabled' | 'failed'
|
||||
name: string
|
||||
tools: number
|
||||
transport: string
|
||||
}
|
||||
|
||||
export interface ProjectInfo {
|
||||
id: string
|
||||
name: string
|
||||
primary_path?: null | string
|
||||
slug: string
|
||||
}
|
||||
export type McpServerStatus = GatewayMcpServerStatus
|
||||
export type ProjectInfo = GatewayProjectInfo
|
||||
|
||||
export interface SessionInfo {
|
||||
cwd?: string
|
||||
|
|
|
|||
11
uv.lock
generated
11
uv.lock
generated
|
|
@ -1597,6 +1597,7 @@ dev = [
|
|||
{ name = "ruff" },
|
||||
{ name = "setuptools" },
|
||||
{ name = "starlette" },
|
||||
{ name = "ts-type" },
|
||||
{ name = "ty" },
|
||||
]
|
||||
dingtalk = [
|
||||
|
|
@ -1847,6 +1848,7 @@ requires-dist = [
|
|||
{ name = "starlette", marker = "extra == 'web'", specifier = "==1.0.1" },
|
||||
{ name = "supermemory", marker = "extra == 'supermemory'", specifier = "==3.50.0" },
|
||||
{ name = "tenacity", specifier = "==9.1.4" },
|
||||
{ name = "ts-type", marker = "extra == 'dev'", specifier = "==0.3.4" },
|
||||
{ name = "ty", marker = "extra == 'dev'", specifier = "==0.0.21" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'", specifier = "==2025.3" },
|
||||
{ name = "urllib3", specifier = ">=2.7.0,<3" },
|
||||
|
|
@ -4136,6 +4138,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ts-type"
|
||||
version = "0.3.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a9/9a/37a2cc86f00818ba15dc2cace832a8bdde4b22fa48c23b2480a10eae8ca6/ts_type-0.3.4.tar.gz", hash = "sha256:5f2e2a4ba4e5f7f63eb7cc5a0208b868dc066da99c34da891bc3cbfe0f27b19d", size = 18003, upload-time = "2025-05-18T05:44:19.288Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/65/be/61270497b881389a61797ea9a288d4560de80d97cfe9679499e2796e5dbb/ts_type-0.3.4-py3-none-any.whl", hash = "sha256:dc16d431d5f8e3e09b923b38f95037c07d95dbed3116964771e65f4cddca7038", size = 14426, upload-time = "2025-05-18T05:44:17.15Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.21"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue