Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# ts-rs (Rust -> TypeScript) export configuration.
# `cargo test --features ts` (or the frontend `gen:types` script) emits `.ts`
# files into the directory below. Generated types are NOT committed (see
# `.gitignore`); the `prebuild` step regenerates them locally and in CI.
# See docs/plans/step1-ts-rs-integration.md.
[env]
TS_RS_EXPORT_DIR = { value = "src/web-ui/src/generated/api", relative = true }
# Map i64/u64 to `number` (not `bigint`) so generated types match the existing
# frontend convention (timestamps are `number` ms throughout the web UI).
TS_RS_LARGE_INT = "number"
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ src/web-ui/src/generated/version.ts
src/web-ui/src/generated/version-injection.html
src/web-ui/public/version.json

# Generated TypeScript bindings (ts-rs) — single source is Rust schema.rs.
# Regenerated by `npm run gen:types` / build prebuild step.
src/web-ui/src/generated/api/

# Tauri generated files
apps/desktop/gen/
src/apps/desktop/gen/
Expand Down
62 changes: 62 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"src/apps/miniapp-market-server",
"src/apps/skin-market-server",
"src/crates/interfaces/acp",
"src/crates/interfaces/app-server",
"src/crates/interfaces/sdk-host",
"src/crates/adapters/agent-runtime-ipc",
"src/crates/assembly/agent-content",
Expand Down Expand Up @@ -87,6 +88,9 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_yaml = "0.9"

# TypeScript binding generation (schema-first; gated by per-crate `ts` features)
ts-rs = { version = "12", features = ["serde-json-impl", "no-serde-warnings"] }

# Error handling
anyhow = "1.0"
thiserror = "2"
Expand Down
254 changes: 254 additions & 0 deletions docs/architecture/agent-runtime-lifecycle-sequence.md

Large diffs are not rendered by default.

35 changes: 28 additions & 7 deletions scripts/build-web-parallel.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
#!/usr/bin/env node

/**
* Runs the web-ui type-check (tsc --noEmit) and the Vite production build in
* parallel. The two are independent: Vite transpiles with esbuild and never
* consults tsc, so serializing them only added wall-clock time.
* Regenerates TypeScript bindings from the Rust schema, then runs the web-ui
* type-check (tsc --noEmit) and the Vite production build in parallel. The
* latter two are independent: Vite transpiles with esbuild and never consults
* tsc, so serializing them only added wall-clock time.
*
* Both child processes must succeed; if either fails the script exits with a
* non-zero code (after letting the sibling finish so its output is not lost).
* The `gen:types` step (which requires a Rust toolchain) runs serially first so
* the Vite build picks up fresh bindings. Local frontend-only iteration can
* skip it with `pnpm --dir src/web-ui build`. Both child processes must succeed;
* if either fails the script exits with a non-zero code (after letting the
* sibling finish so its output is not lost).
*/

import { spawn } from 'node:child_process';
Expand Down Expand Up @@ -54,13 +58,30 @@ function runPrefixed(prefix, command, args, cwd) {
});
}

// Step 1: regenerate TypeScript bindings from the Rust schema (serial, must
// finish before the Vite build so the frontend picks up fresh types). This is
// the only step that requires a working Rust toolchain; local frontend-only
// iteration can skip it with `pnpm --dir src/web-ui build`.
const genTypesCode = await runPrefixed(
'gen-types',
'pnpm',
['--dir', 'src/web-ui', 'run', 'gen:types'],
ROOT_DIR,
);
if (genTypesCode !== 0) {
process.stderr.write('[build-web-parallel] gen:types failed (see output above)\n');
process.exitCode = 1;
process.exit();
}

// Step 2: type-check and Vite build run in parallel.
const tasks = [
runPrefixed('type-check', 'pnpm', ['run', 'type-check:web'], ROOT_DIR),
runPrefixed('vite-build', 'pnpm', ['--dir', 'src/web-ui', 'build'], ROOT_DIR),
];

const codes = await Promise.all(tasks);
const failed = codes.some((code) => code !== 0);
const buildCodes = await Promise.all(tasks);
const failed = buildCodes.some((code) => code !== 0);
if (failed) {
process.stderr.write('[build-web-parallel] build:web failed (see output above)\n');
}
Expand Down
1 change: 1 addition & 0 deletions scripts/core-boundaries/rules/crate-layout.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const crateLayoutRules = [
{ crateName: 'terminal', layer: 'services', path: 'src/crates/services/terminal' },

{ crateName: 'acp', layer: 'interfaces', path: 'src/crates/interfaces/acp' },
{ crateName: 'app-server', layer: 'interfaces', path: 'src/crates/interfaces/app-server' },
{ crateName: 'sdk-host', layer: 'interfaces', path: 'src/crates/interfaces/sdk-host' },
{ crateName: 'agent-runtime-ipc', layer: 'adapters', path: 'src/crates/adapters/agent-runtime-ipc' },
{ crateName: 'ai-adapters', layer: 'adapters', path: 'src/crates/adapters/ai-adapters' },
Expand Down
11 changes: 11 additions & 0 deletions src/apps/server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ path = "src/main.rs"
[dependencies]
bitfun-core = { path = "../../crates/assembly/core", default-features = false, features = ["product-full"] }

# App-server surface: an in-process JSON-RPC server/client pair over an
# in-memory channel transport. The websocket handler routes agent kernel RPCs
# through this client so agent interfaces uniformly go through app-server.
bitfun-app-server = { path = "../../crates/interfaces/app-server" }
bitfun-agent-runtime = { path = "../../crates/execution/agent-runtime" }
bitfun-events = { path = "../../crates/contracts/events" }

agent-client-protocol = { workspace = true }

# Web framework
axum = { workspace = true }
tower-http = { workspace = true }
Expand All @@ -26,8 +35,10 @@ base64 = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
futures-util = { workspace = true }
futures = { workspace = true }
chrono = { workspace = true }
dirs = { workspace = true }
log = { workspace = true }

[lints]
workspace = true
4 changes: 4 additions & 0 deletions src/apps/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This directory contains the `bitfun-server` application, which serves the web backend runtime for BitFun.

> **Deprecated:** This Web Server was already deprecated before the current App Server refactor. Changes made here
> during that refactor are intended to validate protocol and host boundaries; they do not promise feature completeness,
> Desktop parity, backward compatibility, or production readiness.

If you are looking for **Remote Connect self-hosted relay deployment**, use:

- [Relay Server README](../relay-server/README.md)
Expand Down
32 changes: 32 additions & 0 deletions src/apps/server/src/app_server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! Server-host app-server wiring: build the in-process `BitfunAppServer` from
//! the product-assembled [`AgentRuntime`] and return a cloneable handle.
//!
//! The containing Web Server was already deprecated before this refactor.
//! This wiring exists to validate the App Server boundary and is not required
//! to provide complete legacy Web/Desktop behavior or production compatibility.
//!
//! Under browser-direct ACP-over-WS (Step 2) the server host no longer pairs
//! the app-server with an in-process client over `in_memory_pair`. Instead each
//! WebSocket connection is handed straight to [`BitfunAppServer::serve`] via the
//! [`crate::routes::ws_transport`] `Lines` adapter, so the browser connects
//! directly to the in-process app-server over native JSON-RPC. This module only
//! constructs the [`BitfunAppRuntime`] and wraps it in a [`BitfunAppServer`]
//! (cheap `Clone` via the inner `Arc`); `serve` runs once per WS connection.

use bitfun_agent_runtime::sdk::{AgentEventSource, AgentRuntime};
use bitfun_app_server::{BitfunAppRuntime, BitfunAppServer};

/// Build the in-process `BitfunAppServer` for the Server Host.
///
/// Constructs a [`BitfunAppRuntime`] from the product-assembled `runtime` and
/// its `event_source`, wraps it in a [`BitfunAppServer`] (cheap `Clone`), and
/// returns it. The websocket handler clones this handle once per connection and
/// spawns `serve` on a WS-bridged `Lines` transport.
///
/// The caller must keep the runtime services (coordinator, scheduler, ...) and
/// the `EventQueue` the `event_source` was built from alive for as long as the
/// [`BitfunAppServer`] is in use.
pub(crate) fn build(runtime: AgentRuntime, event_source: AgentEventSource) -> BitfunAppServer {
let app_runtime = BitfunAppRuntime::new(runtime, event_source);
BitfunAppServer::new(app_runtime)
}
18 changes: 13 additions & 5 deletions src/apps/server/src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ use std::sync::Arc;
use tokio::sync::RwLock;

/// Shared application state for the server (mirrors Desktop's AppState).
pub struct ServerAppState {
///
/// Several fields are stored to keep the corresponding services alive (they
/// register global singletons during `initialize`), not because they are read
/// again after initialization.
#[allow(dead_code)]
pub(crate) struct ServerAppState {
pub ai_client_factory: Arc<AIClientFactory>,
pub workspace_service: Arc<workspace::WorkspaceService>,
pub workspace_path: Arc<RwLock<Option<std::path::PathBuf>>>,
Expand All @@ -30,7 +35,7 @@ pub struct ServerAppState {
/// Initialize all core services and return the shared server state.
///
/// The optional `workspace` path, when provided, is opened automatically.
pub async fn initialize(workspace: Option<String>) -> anyhow::Result<Arc<ServerAppState>> {
pub(crate) async fn initialize(workspace: Option<String>) -> anyhow::Result<Arc<ServerAppState>> {
log::info!("Initializing BitFun server core services");

// 1. Global config
Expand Down Expand Up @@ -137,9 +142,12 @@ pub async fn initialize(workspace: Option<String>) -> anyhow::Result<Arc<ServerA
coordination::set_global_scheduler(scheduler.clone());

// Cron service
let cron_service =
bitfun_core::service::cron::CronService::new(path_manager.clone(), scheduler.clone())
.await?;
let cron_service = bitfun_core::service::cron::CronService::new(
path_manager.clone(),
coordinator.clone(),
scheduler.clone(),
)
.await?;
bitfun_core::service::cron::set_global_cron_service(cron_service.clone());
let cron_subscriber = Arc::new(bitfun_core::service::cron::CronEventSubscriber::new(
cron_service.clone(),
Expand Down
Loading