web: expose memory staging view

This commit is contained in:
2026-07-24 14:10:52 +09:00
parent b9067a8110
commit b8cc58ac29
9 changed files with 631 additions and 0 deletions
+1
View File
@@ -9,6 +9,7 @@ pub mod companion;
pub mod config;
pub mod hosts;
pub mod identity;
pub mod memory_staging;
pub mod observation;
pub mod profile_settings;
pub mod records;
@@ -0,0 +1,186 @@
use memory::WorkspaceLayout;
use memory::consolidate::{StagingEntry, list_staging_entries_snapshot};
use memory::extract::StagingRecord;
use memory::schema::{SourceEvidenceRef, SourceRef};
use serde::{Deserialize, Serialize};
const DEFAULT_MEMORY_STAGING_LIMIT: usize = 100;
const MAX_MEMORY_STAGING_LIMIT: usize = 500;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MemoryStagingListResponse {
pub limit: usize,
pub returned_count: usize,
pub total_valid_count: usize,
pub invalid_count: usize,
pub truncated: bool,
pub order: String,
pub record_authority: String,
pub items: Vec<MemoryStagingEntrySummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MemoryStagingEntrySummary {
pub id: String,
pub byte_len: u64,
pub record: MemoryStagingRecordSummary,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MemoryStagingRecordSummary {
pub schema_version: u32,
pub id: String,
pub extract_run_id: String,
pub source: SourceRef,
pub kind: String,
pub claim: String,
pub why_useful: String,
pub staleness: Option<String>,
pub evidence: Vec<MemoryStagingEvidenceSummary>,
pub source_refs: Vec<MemorySourceEvidenceRefSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MemoryStagingEvidenceSummary {
pub id: String,
pub kind: String,
pub entry_range: Option<[u64; 2]>,
pub excerpt: Option<String>,
pub summary: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct MemorySourceEvidenceRefSummary {
pub session_id: Option<String>,
pub segment_id: Option<String>,
pub entry_range: Option<[u64; 2]>,
pub evidence_id: Option<String>,
pub evidence_kind: Option<String>,
pub label: Option<String>,
pub summary: Option<String>,
}
pub fn list_memory_staging(
layout: &WorkspaceLayout,
requested_limit: Option<usize>,
) -> MemoryStagingListResponse {
let limit = requested_limit
.unwrap_or(DEFAULT_MEMORY_STAGING_LIMIT)
.min(MAX_MEMORY_STAGING_LIMIT);
let snapshot = list_staging_entries_snapshot(layout);
let total_valid_count = snapshot.entries.len();
let items = snapshot
.entries
.into_iter()
.take(limit)
.map(memory_staging_entry_summary)
.collect::<Vec<_>>();
let returned_count = items.len();
MemoryStagingListResponse {
limit,
returned_count,
total_valid_count,
invalid_count: snapshot.invalid_count,
truncated: total_valid_count > returned_count,
order: "uuidv7_ascending".to_string(),
record_authority: "workspace_memory_staging".to_string(),
items,
}
}
fn memory_staging_entry_summary(entry: StagingEntry) -> MemoryStagingEntrySummary {
MemoryStagingEntrySummary {
id: entry.id.to_string(),
byte_len: entry.bytes,
record: memory_staging_record_summary(entry.record),
}
}
fn memory_staging_record_summary(record: StagingRecord) -> MemoryStagingRecordSummary {
MemoryStagingRecordSummary {
schema_version: record.schema_version,
id: record.id,
extract_run_id: record.extract_run_id,
source: record.source,
kind: record.kind.as_str().to_string(),
claim: record.claim,
why_useful: record.why_useful,
staleness: record.staleness,
evidence: record
.evidence
.into_iter()
.map(|evidence| MemoryStagingEvidenceSummary {
id: evidence.id,
kind: evidence.kind.as_str().to_string(),
entry_range: evidence.entry_range,
excerpt: evidence.excerpt,
summary: evidence.summary,
})
.collect(),
source_refs: record
.source_refs
.into_iter()
.map(memory_source_evidence_ref_summary)
.collect(),
}
}
fn memory_source_evidence_ref_summary(
source_ref: SourceEvidenceRef,
) -> MemorySourceEvidenceRefSummary {
MemorySourceEvidenceRefSummary {
session_id: source_ref.session_id,
segment_id: source_ref.segment_id,
entry_range: source_ref.entry_range,
evidence_id: source_ref.evidence_id,
evidence_kind: source_ref
.evidence_kind
.map(|evidence_kind| evidence_kind.as_str().to_string()),
label: source_ref.label,
summary: source_ref.summary,
}
}
#[cfg(test)]
mod tests {
use super::*;
use memory::extract::{CandidateKind, ExtractedCandidate, ExtractedPayload, write_staging};
use tempfile::TempDir;
fn source() -> SourceRef {
SourceRef {
segment_id: "segment-1".to_string(),
range: [0, 10],
}
}
fn payload(claim: &str) -> ExtractedPayload {
ExtractedPayload {
candidates: vec![ExtractedCandidate {
kind: CandidateKind::Decision,
claim: claim.to_string(),
why_useful: "useful for future work".to_string(),
staleness: None,
evidence_ids: Vec::new(),
}],
}
}
#[test]
fn lists_memory_staging_records_with_cap() {
let temp = TempDir::new().unwrap();
let layout = WorkspaceLayout::new(temp.path().to_path_buf());
write_staging(&layout, source(), payload("first claim")).unwrap();
write_staging(&layout, source(), payload("second claim")).unwrap();
let response = list_memory_staging(&layout, Some(1));
assert_eq!(response.limit, 1);
assert_eq!(response.returned_count, 1);
assert_eq!(response.total_valid_count, 2);
assert!(response.truncated);
assert_eq!(response.record_authority, "workspace_memory_staging");
assert_eq!(response.items[0].record.kind, "decision");
assert_eq!(response.items[0].record.claim, "first claim");
}
}
+21
View File
@@ -56,6 +56,7 @@ use crate::hosts::{
WorkerSpawnResult, WorkerSpawnWorkingDirectoryRequest, WorkerSummary, WorkerWorkspaceSummary,
};
use crate::identity::WorkspaceIdentity;
use crate::memory_staging::{MemoryStagingListResponse, list_memory_staging};
use crate::observation::{
BackendObservationProxy, ObservationProxyError, RuntimeObservationClient,
RuntimeObservationSource, RuntimeObservationSourceConfig,
@@ -479,6 +480,10 @@ pub fn build_router(api: WorkspaceApi) -> Router {
"/api/w/{workspace_id}/tickets/backend",
post(scoped_ticket_backend_operation),
)
.route(
"/api/w/{workspace_id}/memory/staging",
get(scoped_list_memory_staging),
)
.route(
"/api/w/{workspace_id}/memory/backend",
post(scoped_memory_backend_operation),
@@ -1139,6 +1144,11 @@ struct TicketKanbanQuery {
limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
struct MemoryStagingQuery {
limit: Option<usize>,
}
#[derive(Debug, Deserialize)]
struct TranscriptQuery {
start: Option<usize>,
@@ -1432,6 +1442,17 @@ async fn scoped_ticket_backend_operation(
Ok(Json(response))
}
async fn scoped_list_memory_staging(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Query(query): Query<MemoryStagingQuery>,
) -> ApiResult<Json<MemoryStagingListResponse>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let memory_config = manifest::MemoryConfig::default();
let layout = memory::WorkspaceLayout::resolve(&memory_config, &api.config.workspace_root);
Ok(Json(list_memory_staging(&layout, query.limit)))
}
async fn scoped_memory_backend_operation(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,