- 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>
137 lines
3.5 KiB
Rust
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)),
|
|
}
|
|
}
|