feat: generate bounded subject memory surfaces
This commit is contained in:
@@ -1313,6 +1313,16 @@ where
|
||||
{
|
||||
feature_registry.add_module(memory_lifecycle);
|
||||
}
|
||||
if let Some(surface_lifecycle) = crate::feature::builtin::memory_surface_lifecycle::SubjektivSurfaceLifecycleFeature::from_manifest(
|
||||
worker.manifest_lifecycle_features_enabled(),
|
||||
worker.workspace_client_handle(),
|
||||
spawner_manifest.clone(),
|
||||
worker.llm_client_handle(),
|
||||
prompts.clone(),
|
||||
spawner_workspace_context.clone(),
|
||||
)? {
|
||||
feature_registry.add_module(surface_lifecycle);
|
||||
}
|
||||
if let Some(subjektiv_lifecycle) =
|
||||
crate::feature::builtin::memory_lifecycle::SubjektivLifecycleFeature::from_resolved_config(
|
||||
worker.manifest_lifecycle_features_enabled(),
|
||||
|
||||
@@ -11,6 +11,8 @@ pub mod manage_worker;
|
||||
pub mod memory;
|
||||
pub(crate) mod memory_lifecycle;
|
||||
pub mod memory_staging_output;
|
||||
pub(crate) mod memory_surface_lifecycle;
|
||||
pub(crate) mod memory_surface_output;
|
||||
pub mod merge_request;
|
||||
pub mod objective;
|
||||
pub mod orchestration;
|
||||
|
||||
@@ -93,6 +93,87 @@ impl dyn WorkspaceClient + '_ {
|
||||
) -> Result<MemoryConsolidationOutput, WorkspaceMemoryBackendError> {
|
||||
execute_memory_consolidation(self, operation, true).await
|
||||
}
|
||||
|
||||
pub async fn prepare_subjektiv_memory_surface(
|
||||
&self,
|
||||
) -> Result<server_api::SubjektivSurfacePrepareResponse, WorkspaceMemoryBackendError> {
|
||||
match execute_subjektiv_memory_operation(
|
||||
self,
|
||||
server_api::SubjektivMemoryBackendOperation::PrepareSurface(
|
||||
server_api::SubjektivSurfacePrepareRequest {},
|
||||
),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
server_api::SubjektivMemoryBackendResponse::SurfacePrepared(output) => Ok(output),
|
||||
other => Err(WorkspaceMemoryBackendError::Backend(format!(
|
||||
"unexpected surface preparation response: {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn publish_subjektiv_memory_surface(
|
||||
&self,
|
||||
input: server_api::SubjektivSurfacePublishRequest,
|
||||
) -> Result<server_api::SubjektivSurfacePublishResponse, WorkspaceMemoryBackendError> {
|
||||
match execute_subjektiv_memory_operation(
|
||||
self,
|
||||
server_api::SubjektivMemoryBackendOperation::PublishSurface(input),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
server_api::SubjektivMemoryBackendResponse::SurfacePublished(output) => Ok(output),
|
||||
other => Err(WorkspaceMemoryBackendError::Backend(format!(
|
||||
"unexpected surface publication response: {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn fail_subjektiv_memory_surface(
|
||||
&self,
|
||||
input: server_api::SubjektivSurfaceFailureRequest,
|
||||
) -> Result<server_api::SubjektivSurfaceFailureResponse, WorkspaceMemoryBackendError> {
|
||||
match execute_subjektiv_memory_operation(
|
||||
self,
|
||||
server_api::SubjektivMemoryBackendOperation::FailSurface(input),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
server_api::SubjektivMemoryBackendResponse::SurfaceFailed(output) => Ok(output),
|
||||
other => Err(WorkspaceMemoryBackendError::Backend(format!(
|
||||
"unexpected surface failure response: {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_subjektiv_memory_operation(
|
||||
client: &dyn WorkspaceClient,
|
||||
operation: server_api::SubjektivMemoryBackendOperation,
|
||||
) -> Result<server_api::SubjektivMemoryBackendResponse, WorkspaceMemoryBackendError> {
|
||||
let workspace_id =
|
||||
client
|
||||
.workspace_id()
|
||||
.ok_or_else(|| WorkspaceMemoryBackendError::Unavailable {
|
||||
reason: format!(
|
||||
"workspace client kind `{}` has no workspace id",
|
||||
client.kind()
|
||||
),
|
||||
})?;
|
||||
let response = client.execute(WorkspaceRequest::json(
|
||||
WorkspaceRequestMethod::Post,
|
||||
format!("/api/w/{workspace_id}/subjektiv/memory"),
|
||||
serde_json::to_string(&server_api::SubjektivMemoryBackendRequest { operation })?,
|
||||
))?;
|
||||
let status = reqwest::StatusCode::from_u16(response.status)
|
||||
.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR);
|
||||
if !response.is_success() {
|
||||
return Err(WorkspaceMemoryBackendError::Http {
|
||||
status,
|
||||
body: response.body,
|
||||
});
|
||||
}
|
||||
Ok(serde_json::from_str(&response.body)?)
|
||||
}
|
||||
|
||||
async fn execute_memory_backend(
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use agen::llm_client::LlmClient;
|
||||
use arc_swap::ArcSwap;
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::PromptCatalog;
|
||||
use crate::Scope;
|
||||
use crate::feature::background::{
|
||||
BackgroundTaskCancellation, BackgroundTaskContext, BackgroundTaskSpec, BackgroundTaskTrigger,
|
||||
FeatureBackgroundTask,
|
||||
};
|
||||
use crate::feature::builtin::memory::WorkspaceMemoryBackendError;
|
||||
use crate::feature::builtin::memory_surface_output::{
|
||||
MemorySurfaceOutputFeature, MemorySurfaceOutputState,
|
||||
};
|
||||
use crate::feature::{
|
||||
BackgroundTaskDeclaration, FeatureDescriptor, FeatureInstallContext, FeatureInstallError,
|
||||
FeatureModule, FeatureRegistryBuilder,
|
||||
};
|
||||
use crate::hook::{HookError, HookErrorCategory};
|
||||
use crate::internal_worker::{
|
||||
InternalWorkerAuthority, InternalWorkerIdentity, InternalWorkerSpec,
|
||||
run_internal_worker_with_cancel_sender,
|
||||
};
|
||||
use crate::worker::{WorkerFilesystemAuthority, WorkerWorkspaceContext, WorkspaceClient};
|
||||
use manifest::WorkerManifest;
|
||||
|
||||
const TASK_NAME: &str = "subjektiv-memory-surface";
|
||||
const TASK_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
const MAX_GENERATION_ATTEMPTS: usize = 2;
|
||||
const EDITOR_MAX_TURNS: u32 = 3;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct SubjektivSurfaceLifecycleFeature {
|
||||
task: SubjektivSurfaceLifecycleTask,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SubjektivSurfaceLifecycleTask {
|
||||
workspace_client: Arc<dyn WorkspaceClient>,
|
||||
manifest: WorkerManifest,
|
||||
client: Box<dyn LlmClient>,
|
||||
prompts: Arc<ArcSwap<PromptCatalog>>,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
}
|
||||
|
||||
impl SubjektivSurfaceLifecycleFeature {
|
||||
pub(crate) fn from_manifest(
|
||||
lifecycle_enabled: bool,
|
||||
workspace_client: Arc<dyn WorkspaceClient>,
|
||||
manifest: WorkerManifest,
|
||||
client: Box<dyn LlmClient>,
|
||||
prompts: Arc<ArcSwap<PromptCatalog>>,
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
) -> std::io::Result<Option<Self>> {
|
||||
let dedicated = manifest.profile.as_ref().is_some_and(|snapshot| {
|
||||
matches!(
|
||||
&snapshot.source,
|
||||
manifest::ProfileSource::Registry {
|
||||
source: manifest::ProfileRegistrySource::Builtin,
|
||||
name,
|
||||
..
|
||||
} if name == "subjektiv-memory-consolidation"
|
||||
)
|
||||
});
|
||||
if !lifecycle_enabled || !dedicated {
|
||||
return Ok(None);
|
||||
}
|
||||
if !workspace_client.is_available() || workspace_client.workspace_id().is_none() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::InvalidInput,
|
||||
"subjektiv surface generation requires Backend Workspace API authority",
|
||||
));
|
||||
}
|
||||
Ok(Some(Self {
|
||||
task: SubjektivSurfaceLifecycleTask {
|
||||
workspace_client,
|
||||
manifest,
|
||||
client,
|
||||
prompts,
|
||||
workspace_context,
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl FeatureModule for SubjektivSurfaceLifecycleFeature {
|
||||
fn descriptor(&self) -> FeatureDescriptor {
|
||||
FeatureDescriptor::builtin("subjektiv-memory-surface-lifecycle", "subjektiv Memory Surface Lifecycle")
|
||||
.with_description("Rebuilds one bounded subject Memory surface after each committed consolidation run.")
|
||||
.with_background_task(BackgroundTaskDeclaration::worker_managed(
|
||||
TASK_NAME,
|
||||
"Generate and publish a current subject Memory surface from bounded confirmed revisions.",
|
||||
))
|
||||
}
|
||||
|
||||
fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> {
|
||||
let declaration = BackgroundTaskDeclaration::worker_managed(
|
||||
TASK_NAME,
|
||||
"Generate and publish a current subject Memory surface from bounded confirmed revisions.",
|
||||
);
|
||||
let mut spec = BackgroundTaskSpec::single_flight(declaration, TASK_TIMEOUT);
|
||||
spec.trigger = BackgroundTaskTrigger::RunCommitted;
|
||||
context.background_tasks().register(spec, self.task.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl FeatureBackgroundTask for SubjektivSurfaceLifecycleTask {
|
||||
async fn run(
|
||||
&self,
|
||||
context: BackgroundTaskContext,
|
||||
cancellation: BackgroundTaskCancellation,
|
||||
) -> Result<(), HookError> {
|
||||
for attempt in 0..MAX_GENERATION_ATTEMPTS {
|
||||
context.generation_fence.ensure_current()?;
|
||||
if cancellation.is_cancelled() {
|
||||
return Ok(());
|
||||
}
|
||||
let generation = self
|
||||
.workspace_client
|
||||
.prepare_subjektiv_memory_surface()
|
||||
.await
|
||||
.map_err(surface_hook_error)?;
|
||||
if generation.materials.is_empty() && generation.active_memory_count > 0 {
|
||||
self.record_failure(&generation.generation_id, "input_budget_exhausted")
|
||||
.await;
|
||||
tracing::warn!(
|
||||
active_memory_count = generation.active_memory_count,
|
||||
"subject Memory surface materials could not fit the bounded input"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
let points = if generation.materials.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
match self.edit_surface(&generation, cancellation.clone()).await {
|
||||
Ok(points) => points,
|
||||
Err(error) => {
|
||||
self.record_failure(&generation.generation_id, "editor_failed")
|
||||
.await;
|
||||
tracing::warn!(%error, "subject Memory surface editor failed");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
context.generation_fence.ensure_current()?;
|
||||
if cancellation.is_cancelled() {
|
||||
return Ok(());
|
||||
}
|
||||
let publish = self
|
||||
.workspace_client
|
||||
.publish_subjektiv_memory_surface(server_api::SubjektivSurfacePublishRequest {
|
||||
generation_id: generation.generation_id.clone(),
|
||||
points,
|
||||
})
|
||||
.await;
|
||||
match publish {
|
||||
Ok(output) => {
|
||||
tracing::debug!(
|
||||
snapshot_id = output.snapshot_id,
|
||||
store_revision = output.built_from_store_revision,
|
||||
empty = output.empty,
|
||||
"published subject Memory surface"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Err(WorkspaceMemoryBackendError::Http { status, .. })
|
||||
if status == reqwest::StatusCode::CONFLICT
|
||||
&& attempt + 1 < MAX_GENERATION_ATTEMPTS =>
|
||||
{
|
||||
// Confirmed Memory moved while editing. Discard the stale
|
||||
// output and rebuild once from a fresh bounded generation.
|
||||
continue;
|
||||
}
|
||||
Err(error) => {
|
||||
self.record_failure(&generation.generation_id, "publish_failed")
|
||||
.await;
|
||||
tracing::warn!(%error, "subject Memory surface publication failed");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SubjektivSurfaceLifecycleTask {
|
||||
async fn edit_surface(
|
||||
&self,
|
||||
generation: &server_api::SubjektivSurfacePrepareResponse,
|
||||
cancellation: BackgroundTaskCancellation,
|
||||
) -> Result<Vec<server_api::SubjektivSurfacePoint>, HookError> {
|
||||
let language = self
|
||||
.manifest
|
||||
.feature
|
||||
.memory
|
||||
.workspace_settings()
|
||||
.map(|settings| settings.language)
|
||||
.ok_or_else(|| {
|
||||
HookError::new(
|
||||
HookErrorCategory::Internal,
|
||||
"surface editor requires bound Workspace Memory settings",
|
||||
)
|
||||
})?;
|
||||
let system_prompt = self
|
||||
.prompts
|
||||
.load_full()
|
||||
.subjektiv_memory_surface_system(&language)
|
||||
.map_err(|error| HookError::new(HookErrorCategory::Internal, error.to_string()))?;
|
||||
let input = serde_json::to_string_pretty(&serde_json::json!({
|
||||
"question": "この主体が次の作業を始める際、毎回思い出しておくべきことは何か。継続的な制約、判断の前提、未解決事項、再発を避けたい教訓を、指定予算内でまとめる。",
|
||||
"body_token_budget": generation.body_token_budget,
|
||||
"input_token_budget": generation.input_token_budget,
|
||||
"materials": generation.materials,
|
||||
}))
|
||||
.map_err(|error| HookError::new(HookErrorCategory::Internal, error.to_string()))?;
|
||||
let output_state = MemorySurfaceOutputState::new(
|
||||
generation.materials.clone(),
|
||||
generation.body_token_budget,
|
||||
);
|
||||
let features = FeatureRegistryBuilder::new()
|
||||
.with_module(MemorySurfaceOutputFeature::new(output_state.clone()));
|
||||
let cancellation_observer = cancellation.clone();
|
||||
let cancel_observer = move |sender: tokio::sync::mpsc::Sender<()>| {
|
||||
tokio::spawn(async move {
|
||||
cancellation_observer.cancelled().await;
|
||||
let _ = sender.send(()).await;
|
||||
});
|
||||
};
|
||||
let result = run_internal_worker_with_cancel_sender(
|
||||
InternalWorkerSpec {
|
||||
identity: InternalWorkerIdentity {
|
||||
kind: "subjektiv-memory-surface",
|
||||
run_id: uuid::Uuid::now_v7(),
|
||||
},
|
||||
manifest: self.manifest.clone(),
|
||||
client: self.client.clone_boxed(),
|
||||
system_prompt,
|
||||
input,
|
||||
cache_key: None,
|
||||
max_turns: Some(EDITOR_MAX_TURNS),
|
||||
engine_configurator: None,
|
||||
features,
|
||||
required_tools: &["SubmitMemorySurface"],
|
||||
authority: InternalWorkerAuthority {
|
||||
workspace: self.workspace_context.clone(),
|
||||
filesystem: WorkerFilesystemAuthority::None,
|
||||
scope: Scope::empty(),
|
||||
workdir_session: None,
|
||||
},
|
||||
},
|
||||
cancel_observer,
|
||||
)
|
||||
.await;
|
||||
if let Err(error) = result {
|
||||
return Err(HookError::new(
|
||||
HookErrorCategory::Internal,
|
||||
format!("surface editor Internal Worker failed: {}", error.source),
|
||||
));
|
||||
}
|
||||
output_state.submitted().ok_or_else(|| {
|
||||
HookError::new(
|
||||
HookErrorCategory::Internal,
|
||||
"surface editor finished without valid structured output",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
async fn record_failure(&self, generation_id: &str, reason_code: &str) {
|
||||
if let Err(error) = self
|
||||
.workspace_client
|
||||
.fail_subjektiv_memory_surface(server_api::SubjektivSurfaceFailureRequest {
|
||||
generation_id: generation_id.to_string(),
|
||||
reason_code: reason_code.to_string(),
|
||||
})
|
||||
.await
|
||||
{
|
||||
tracing::debug!(%error, "could not record subject Memory surface failure");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn surface_hook_error(error: WorkspaceMemoryBackendError) -> HookError {
|
||||
HookError::new(HookErrorCategory::Internal, error.to_string())
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use agen::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use async_trait::async_trait;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::feature::{
|
||||
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ToolContribution,
|
||||
ToolDeclaration,
|
||||
};
|
||||
|
||||
const SUBMIT_TOOL: &str = "SubmitMemorySurface";
|
||||
const SUBMIT_DESCRIPTION: &str = "Submit a bounded Markdown Memory surface whose every point cites exact refs from the supplied confirmed-Memory materials. Reference validation proves existence and scope, not semantic correctness.";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct MemorySurfaceOutputState {
|
||||
materials: Arc<Vec<server_api::SubjektivSurfaceMaterial>>,
|
||||
body_token_budget: usize,
|
||||
submitted: Arc<Mutex<Option<Vec<server_api::SubjektivSurfacePoint>>>>,
|
||||
}
|
||||
|
||||
impl MemorySurfaceOutputState {
|
||||
pub(crate) fn new(
|
||||
materials: Vec<server_api::SubjektivSurfaceMaterial>,
|
||||
body_token_budget: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
materials: Arc::new(materials),
|
||||
body_token_budget,
|
||||
submitted: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn submitted(&self) -> Option<Vec<server_api::SubjektivSurfacePoint>> {
|
||||
self.submitted
|
||||
.lock()
|
||||
.expect("Memory surface output state poisoned")
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn validate(&self, points: &[server_api::SubjektivSurfacePoint]) -> Result<(), ToolError> {
|
||||
if self.materials.is_empty() {
|
||||
return if points.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ToolError::InvalidArgument(
|
||||
"empty materials require an empty points array".into(),
|
||||
))
|
||||
};
|
||||
}
|
||||
if points.is_empty() {
|
||||
return Err(ToolError::InvalidArgument(
|
||||
"non-empty materials require at least one grounded point".into(),
|
||||
));
|
||||
}
|
||||
if points.len() > 24 {
|
||||
return Err(ToolError::InvalidArgument(format!(
|
||||
"points count {} exceeds the configured limit 24",
|
||||
points.len()
|
||||
)));
|
||||
}
|
||||
let allowed = self
|
||||
.materials
|
||||
.iter()
|
||||
.map(|material| (material.memory_id.as_str(), material.revision))
|
||||
.collect::<HashSet<_>>();
|
||||
let mut bodies = Vec::with_capacity(points.len());
|
||||
for point in points {
|
||||
let body = point.body_md.trim();
|
||||
if body.is_empty() {
|
||||
return Err(ToolError::InvalidArgument(
|
||||
"surface point body_md must not be empty".into(),
|
||||
));
|
||||
}
|
||||
if point.memory_refs.is_empty() {
|
||||
return Err(ToolError::InvalidArgument(
|
||||
"every surface point requires at least one memory_ref".into(),
|
||||
));
|
||||
}
|
||||
let mut point_refs = HashSet::new();
|
||||
for reference in &point.memory_refs {
|
||||
if !point_refs.insert((reference.memory_id.as_str(), reference.revision)) {
|
||||
return Err(ToolError::InvalidArgument(format!(
|
||||
"duplicate surface point ref {}@{}",
|
||||
reference.memory_id, reference.revision
|
||||
)));
|
||||
}
|
||||
if !allowed.contains(&(reference.memory_id.as_str(), reference.revision)) {
|
||||
return Err(ToolError::InvalidArgument(format!(
|
||||
"surface ref {}@{} is not in the generation materials",
|
||||
reference.memory_id, reference.revision
|
||||
)));
|
||||
}
|
||||
}
|
||||
bodies.push(body);
|
||||
}
|
||||
let bytes = bodies
|
||||
.iter()
|
||||
.map(|body| body.len())
|
||||
.sum::<usize>()
|
||||
.saturating_add(points.len().saturating_sub(1) * 2);
|
||||
let tokens = bytes.saturating_add(3) / 4;
|
||||
if tokens > self.body_token_budget {
|
||||
return Err(ToolError::InvalidArgument(format!(
|
||||
"surface body token estimate {tokens} exceeds budget {}",
|
||||
self.body_token_budget
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct MemorySurfaceOutputFeature {
|
||||
state: MemorySurfaceOutputState,
|
||||
}
|
||||
|
||||
impl MemorySurfaceOutputFeature {
|
||||
pub(crate) fn new(state: MemorySurfaceOutputState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
impl FeatureModule for MemorySurfaceOutputFeature {
|
||||
fn descriptor(&self) -> FeatureDescriptor {
|
||||
FeatureDescriptor::builtin("memory-surface-output", "Memory Surface Output")
|
||||
.with_description(
|
||||
"Restricted structured output for one clean-context Memory surface editor.",
|
||||
)
|
||||
.with_tool(ToolDeclaration::new(SUBMIT_TOOL, SUBMIT_DESCRIPTION))
|
||||
}
|
||||
|
||||
fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> {
|
||||
context.tools().register(ToolContribution::new(
|
||||
SUBMIT_TOOL,
|
||||
submit_definition(self.state.clone()),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn submit_definition(state: MemorySurfaceOutputState) -> ToolDefinition {
|
||||
Arc::new(move || {
|
||||
let schema = serde_json::to_value(schemars::schema_for!(SubmitMemorySurfaceParams))
|
||||
.unwrap_or_else(|_| serde_json::json!({}));
|
||||
let meta = ToolMeta::new(SUBMIT_TOOL)
|
||||
.description(SUBMIT_DESCRIPTION)
|
||||
.input_schema(schema);
|
||||
let tool: Arc<dyn Tool> = Arc::new(SubmitMemorySurfaceTool {
|
||||
state: state.clone(),
|
||||
});
|
||||
(meta, tool)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct SubmitMemorySurfaceParams {
|
||||
points: Vec<server_api::SubjektivSurfacePoint>,
|
||||
}
|
||||
|
||||
struct SubmitMemorySurfaceTool {
|
||||
state: MemorySurfaceOutputState,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Tool for SubmitMemorySurfaceTool {
|
||||
async fn execute(
|
||||
&self,
|
||||
input_json: &str,
|
||||
_context: agen::tool::ToolExecutionContext,
|
||||
) -> Result<ToolOutput, ToolError> {
|
||||
let params: SubmitMemorySurfaceParams =
|
||||
serde_json::from_str(input_json).map_err(|error| {
|
||||
ToolError::InvalidArgument(format!("invalid SubmitMemorySurface input: {error}"))
|
||||
})?;
|
||||
self.state.validate(¶ms.points)?;
|
||||
let mut submitted = self
|
||||
.state
|
||||
.submitted
|
||||
.lock()
|
||||
.expect("Memory surface output state poisoned");
|
||||
if submitted.is_some() {
|
||||
return Err(ToolError::InvalidArgument(
|
||||
"SubmitMemorySurface already accepted output for this generation".into(),
|
||||
));
|
||||
}
|
||||
let count = params.points.len();
|
||||
*submitted = Some(params.points);
|
||||
Ok(ToolOutput {
|
||||
summary: format!("Accepted {count} grounded Memory surface point(s)."),
|
||||
content: None,
|
||||
attachments: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use memory::extract::CandidateKind;
|
||||
|
||||
fn material(id: &str) -> server_api::SubjektivSurfaceMaterial {
|
||||
server_api::SubjektivSurfaceMaterial {
|
||||
memory_id: id.into(),
|
||||
revision: 1,
|
||||
kind: CandidateKind::Constraint,
|
||||
body_md: "Keep the boundary.".into(),
|
||||
why_useful: "Avoid regression.".into(),
|
||||
staleness: Some("Until the boundary changes.".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn point(id: &str) -> server_api::SubjektivSurfacePoint {
|
||||
server_api::SubjektivSurfacePoint {
|
||||
body_md: "- Keep the boundary unless it changes.".into(),
|
||||
memory_refs: vec![server_api::SubjektivMemoryRevisionRef {
|
||||
memory_id: id.into(),
|
||||
revision: 1,
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_or_material_external_refs_and_budget_overflow() {
|
||||
let state = MemorySurfaceOutputState::new(vec![material("allowed")], 32);
|
||||
assert!(state.validate(&[]).is_err());
|
||||
assert!(state.validate(&[point("other")]).is_err());
|
||||
let mut oversized = point("allowed");
|
||||
oversized.body_md = "x".repeat(132);
|
||||
assert!(state.validate(&[oversized]).is_err());
|
||||
assert!(state.validate(&[point("allowed")]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_materials_accept_only_normal_empty_output() {
|
||||
let state = MemorySurfaceOutputState::new(Vec::new(), 32);
|
||||
assert!(state.validate(&[]).is_ok());
|
||||
assert!(state.validate(&[point("invented")]).is_err());
|
||||
}
|
||||
}
|
||||
@@ -95,6 +95,7 @@ pub enum WorkerPrompt {
|
||||
MemoryExtractSystem,
|
||||
MemoryConsolidationSystem,
|
||||
SubjektivMemoryConsolidationSystem,
|
||||
SubjektivMemorySurfaceSystem,
|
||||
FlowVerifierSystem,
|
||||
BackendJobSystem,
|
||||
NotifyWrapper,
|
||||
@@ -116,6 +117,7 @@ impl WorkerPrompt {
|
||||
Self::SubjektivMemoryConsolidationSystem => {
|
||||
"internal.subjektiv_memory_consolidation_system"
|
||||
}
|
||||
Self::SubjektivMemorySurfaceSystem => "internal.subjektiv_memory_surface_system",
|
||||
Self::FlowVerifierSystem => "internal.flow_verifier_system",
|
||||
Self::BackendJobSystem => "internal.backend_job_system",
|
||||
Self::NotifyWrapper => "internal.notify_wrapper",
|
||||
@@ -136,6 +138,7 @@ impl WorkerPrompt {
|
||||
WorkerPrompt::MemoryExtractSystem,
|
||||
WorkerPrompt::MemoryConsolidationSystem,
|
||||
WorkerPrompt::SubjektivMemoryConsolidationSystem,
|
||||
WorkerPrompt::SubjektivMemorySurfaceSystem,
|
||||
WorkerPrompt::FlowVerifierSystem,
|
||||
WorkerPrompt::BackendJobSystem,
|
||||
WorkerPrompt::NotifyWrapper,
|
||||
@@ -463,6 +466,12 @@ impl PromptCatalog {
|
||||
single("language", language),
|
||||
)
|
||||
}
|
||||
pub fn subjektiv_memory_surface_system(&self, language: &str) -> Result<String, CatalogError> {
|
||||
self.render(
|
||||
WorkerPrompt::SubjektivMemorySurfaceSystem,
|
||||
single("language", language),
|
||||
)
|
||||
}
|
||||
pub fn flow_verifier_system(&self) -> Result<String, CatalogError> {
|
||||
self.render(WorkerPrompt::FlowVerifierSystem, Value::UNDEFINED)
|
||||
}
|
||||
@@ -927,6 +936,23 @@ mod tests {
|
||||
.unwrap()
|
||||
.contains("LANGUAGE_MARKER")
|
||||
);
|
||||
let surface = catalog
|
||||
.subjektiv_memory_surface_system("SURFACE_LANGUAGE")
|
||||
.unwrap();
|
||||
assert!(surface.contains("SURFACE_LANGUAGE"));
|
||||
for required in [
|
||||
"conditions",
|
||||
"negation",
|
||||
"contradictions",
|
||||
"staleness",
|
||||
"SubmitMemorySurface",
|
||||
"not semantic verification",
|
||||
] {
|
||||
assert!(
|
||||
surface.contains(required),
|
||||
"missing surface rule {required}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
catalog
|
||||
.notify_wrapper("NOTIFICATION_MARKER")
|
||||
|
||||
Reference in New Issue
Block a user