lakehouse/crates/queryd/src/workspace_service.rs
root 0b9da45647 Agent workspaces: per-contract overlays with instant handoff
- WorkspaceManager: create/get/list workspaces with daily/weekly/monthly/pinned tiers
- Saved searches: agent stores SQL queries in workspace context
- Shortlist: tag candidates/records to a workspace with notes
- Activity log: track calls, emails, updates per workspace per agent
- Instant handoff: transfer workspace ownership with full history
  Zero data copy — just a pointer swap, receiving agent sees everything
- Persistence: workspaces stored as JSON in object storage, rebuilt on startup
- Endpoints: /workspaces/create, /{id}, /{id}/handoff, /{id}/search,
  /{id}/shortlist, /{id}/activity
- Tested: Sarah creates workspace, saves searches, shortlists 3 candidates,
  logs activity, hands off to Mike who continues seamlessly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 08:44:45 -05:00

137 lines
3.5 KiB
Rust

use axum::{
Json, Router,
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post},
};
use serde::Deserialize;
use crate::workspace::{Tier, WorkspaceManager};
pub fn router(manager: WorkspaceManager) -> Router {
Router::new()
.route("/", get(list_workspaces))
.route("/create", post(create_workspace))
.route("/{id}", get(get_workspace))
.route("/{id}/handoff", post(handoff))
.route("/{id}/search", post(add_search))
.route("/{id}/shortlist", post(add_to_shortlist))
.route("/{id}/activity", post(log_activity))
.with_state(manager)
}
#[derive(Deserialize)]
struct ListQuery {
owner: Option<String>,
tier: Option<Tier>,
}
async fn list_workspaces(
State(mgr): State<WorkspaceManager>,
Query(q): Query<ListQuery>,
) -> impl IntoResponse {
let workspaces = mgr.list(q.owner.as_deref(), q.tier.as_ref()).await;
Json(workspaces)
}
#[derive(Deserialize)]
struct CreateRequest {
name: String,
description: String,
owner: String,
tier: Tier,
}
async fn create_workspace(
State(mgr): State<WorkspaceManager>,
Json(req): Json<CreateRequest>,
) -> impl IntoResponse {
match mgr.create(req.name, req.description, req.owner, req.tier).await {
Ok(ws) => Ok((StatusCode::CREATED, Json(ws))),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
}
}
async fn get_workspace(
State(mgr): State<WorkspaceManager>,
Path(id): Path<String>,
) -> impl IntoResponse {
match mgr.get(&id).await {
Some(ws) => Ok(Json(ws)),
None => Err((StatusCode::NOT_FOUND, format!("workspace not found: {id}"))),
}
}
#[derive(Deserialize)]
struct HandoffRequest {
to_agent: String,
reason: String,
}
async fn handoff(
State(mgr): State<WorkspaceManager>,
Path(id): Path<String>,
Json(req): Json<HandoffRequest>,
) -> impl IntoResponse {
match mgr.handoff(&id, &req.to_agent, &req.reason).await {
Ok(ws) => Ok(Json(ws)),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
}
}
#[derive(Deserialize)]
struct SearchRequest {
name: String,
sql: String,
agent: String,
}
async fn add_search(
State(mgr): State<WorkspaceManager>,
Path(id): Path<String>,
Json(req): Json<SearchRequest>,
) -> impl IntoResponse {
match mgr.add_search(&id, req.name, req.sql, &req.agent).await {
Ok(()) => Ok((StatusCode::OK, "search saved")),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
}
}
#[derive(Deserialize)]
struct ShortlistRequest {
dataset: String,
record_id: String,
notes: String,
agent: String,
}
async fn add_to_shortlist(
State(mgr): State<WorkspaceManager>,
Path(id): Path<String>,
Json(req): Json<ShortlistRequest>,
) -> impl IntoResponse {
match mgr.add_to_shortlist(&id, req.dataset, req.record_id, req.notes, &req.agent).await {
Ok(()) => Ok((StatusCode::OK, "added to shortlist")),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
}
}
#[derive(Deserialize)]
struct ActivityRequest {
action: String,
detail: String,
agent: String,
}
async fn log_activity(
State(mgr): State<WorkspaceManager>,
Path(id): Path<String>,
Json(req): Json<ActivityRequest>,
) -> impl IntoResponse {
match mgr.log_activity(&id, req.action, req.detail, &req.agent).await {
Ok(()) => Ok((StatusCode::OK, "activity logged")),
Err(e) => Err((StatusCode::INTERNAL_SERVER_ERROR, e)),
}
}