swarm repositories / source
aboutsummaryrefslogtreecommitdiff
path: root/crates/fidget-spinner-cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fidget-spinner-cli/src')
-rw-r--r--crates/fidget-spinner-cli/src/main.rs207
-rw-r--r--crates/fidget-spinner-cli/src/mcp/catalog.rs104
-rw-r--r--crates/fidget-spinner-cli/src/mcp/service.rs313
3 files changed, 477 insertions, 147 deletions
diff --git a/crates/fidget-spinner-cli/src/main.rs b/crates/fidget-spinner-cli/src/main.rs
index 7711cb4..491e30d 100644
--- a/crates/fidget-spinner-cli/src/main.rs
+++ b/crates/fidget-spinner-cli/src/main.rs
@@ -17,9 +17,10 @@ use fidget_spinner_core::{
};
use fidget_spinner_store_sqlite::{
CloseExperimentRequest, CreateFrontierRequest, CreateNodeRequest, DefineMetricRequest,
- DefineRunDimensionRequest, EdgeAttachment, EdgeAttachmentDirection, ListNodesQuery,
- MetricBestQuery, MetricFieldSource, MetricKeyQuery, MetricRankOrder, ProjectStore,
- RemoveSchemaFieldRequest, STORE_DIR_NAME, StoreError, UpsertSchemaFieldRequest,
+ DefineRunDimensionRequest, EdgeAttachment, EdgeAttachmentDirection, ExperimentAnalysisDraft,
+ ListNodesQuery, MetricBestQuery, MetricFieldSource, MetricKeyQuery, MetricRankOrder,
+ OpenExperimentRequest, ProjectStore, RemoveSchemaFieldRequest, STORE_DIR_NAME, StoreError,
+ UpsertSchemaFieldRequest,
};
use serde::Serialize;
use serde_json::{Map, Value, json};
@@ -57,13 +58,15 @@ enum Command {
},
/// Record terse off-path notes.
Note(NoteCommand),
+ /// Record core-path hypotheses before experimental work begins.
+ Hypothesis(HypothesisCommand),
/// Manage the repo-local tag registry.
Tag {
#[command(subcommand)]
command: TagCommand,
},
- /// Record off-path research and enabling work.
- Research(ResearchCommand),
+ /// Record imported sources and documentary context.
+ Source(SourceCommand),
/// Inspect rankable metrics across closed experiments.
Metric {
#[command(subcommand)]
@@ -186,10 +189,10 @@ struct NodeAddArgs {
#[arg(long)]
title: String,
#[arg(long)]
- /// Required for `note` and `research` nodes.
+ /// Required for `note` and `source` nodes.
summary: Option<String>,
#[arg(long = "payload-json")]
- /// JSON object payload. `note` and `research` nodes require a non-empty `body` string.
+ /// JSON object payload. `note` and `source` nodes require a non-empty `body` string.
payload_json: Option<String>,
#[arg(long = "payload-file")]
payload_file: Option<PathBuf>,
@@ -263,6 +266,12 @@ struct NoteCommand {
command: NoteSubcommand,
}
+#[derive(Args)]
+struct HypothesisCommand {
+ #[command(subcommand)]
+ command: HypothesisSubcommand,
+}
+
#[derive(Subcommand)]
enum NoteSubcommand {
/// Record a quick off-path note.
@@ -270,6 +279,12 @@ enum NoteSubcommand {
}
#[derive(Subcommand)]
+enum HypothesisSubcommand {
+ /// Record a core-path hypothesis with low ceremony.
+ Add(QuickHypothesisArgs),
+}
+
+#[derive(Subcommand)]
enum TagCommand {
/// Register a new repo-local tag.
Add(TagAddArgs),
@@ -278,15 +293,15 @@ enum TagCommand {
}
#[derive(Args)]
-struct ResearchCommand {
+struct SourceCommand {
#[command(subcommand)]
- command: ResearchSubcommand,
+ command: SourceSubcommand,
}
#[derive(Subcommand)]
-enum ResearchSubcommand {
- /// Record off-path research or enabling work.
- Add(QuickResearchArgs),
+enum SourceSubcommand {
+ /// Record imported source material or documentary context.
+ Add(QuickSourceArgs),
}
#[derive(Subcommand)]
@@ -376,6 +391,22 @@ struct QuickNoteArgs {
}
#[derive(Args)]
+struct QuickHypothesisArgs {
+ #[command(flatten)]
+ project: ProjectArg,
+ #[arg(long)]
+ frontier: String,
+ #[arg(long)]
+ title: String,
+ #[arg(long)]
+ summary: String,
+ #[arg(long)]
+ body: String,
+ #[arg(long = "parent")]
+ parents: Vec<String>,
+}
+
+#[derive(Args)]
struct TagAddArgs {
#[command(flatten)]
project: ProjectArg,
@@ -386,7 +417,7 @@ struct TagAddArgs {
}
#[derive(Args)]
-struct QuickResearchArgs {
+struct QuickSourceArgs {
#[command(flatten)]
project: ProjectArg,
#[arg(long)]
@@ -459,8 +490,12 @@ struct MetricBestArgs {
#[derive(Subcommand)]
enum ExperimentCommand {
+ /// Open a stateful experiment against one hypothesis and base checkpoint.
+ Open(ExperimentOpenArgs),
+ /// List open experiments, optionally narrowed to one frontier.
+ List(ExperimentListArgs),
/// Close a core-path experiment with checkpoint, run, note, and verdict.
- Close(ExperimentCloseArgs),
+ Close(Box<ExperimentCloseArgs>),
}
#[derive(Subcommand)]
@@ -481,12 +516,8 @@ enum UiCommand {
struct ExperimentCloseArgs {
#[command(flatten)]
project: ProjectArg,
- #[arg(long)]
- frontier: String,
- #[arg(long = "base-checkpoint")]
- base_checkpoint: String,
- #[arg(long = "change-node")]
- change_node: String,
+ #[arg(long = "experiment")]
+ experiment_id: String,
#[arg(long = "candidate-summary")]
candidate_summary: String,
#[arg(long = "run-title")]
@@ -518,12 +549,42 @@ struct ExperimentCloseArgs {
next_hypotheses: Vec<String>,
#[arg(long = "verdict", value_enum)]
verdict: CliFrontierVerdict,
+ #[arg(long = "analysis-title")]
+ analysis_title: Option<String>,
+ #[arg(long = "analysis-summary")]
+ analysis_summary: Option<String>,
+ #[arg(long = "analysis-body")]
+ analysis_body: Option<String>,
#[arg(long = "decision-title")]
decision_title: String,
#[arg(long = "decision-rationale")]
decision_rationale: String,
}
+#[derive(Args)]
+struct ExperimentOpenArgs {
+ #[command(flatten)]
+ project: ProjectArg,
+ #[arg(long)]
+ frontier: String,
+ #[arg(long = "base-checkpoint")]
+ base_checkpoint: String,
+ #[arg(long = "hypothesis-node")]
+ hypothesis_node: String,
+ #[arg(long)]
+ title: String,
+ #[arg(long)]
+ summary: Option<String>,
+}
+
+#[derive(Args)]
+struct ExperimentListArgs {
+ #[command(flatten)]
+ project: ProjectArg,
+ #[arg(long)]
+ frontier: Option<String>,
+}
+
#[derive(Subcommand)]
enum SkillCommand {
/// List bundled skills.
@@ -588,12 +649,11 @@ struct UiServeArgs {
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum CliNodeClass {
Contract,
- Change,
+ Hypothesis,
Run,
Analysis,
Decision,
- Research,
- Enabling,
+ Source,
Note,
}
@@ -623,7 +683,7 @@ enum CliExecutionBackend {
#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
enum CliMetricSource {
RunMetric,
- ChangePayload,
+ HypothesisPayload,
RunPayload,
AnalysisPayload,
DecisionPayload,
@@ -713,12 +773,15 @@ fn run() -> Result<(), StoreError> {
Command::Note(command) => match command.command {
NoteSubcommand::Quick(args) => run_quick_note(args),
},
+ Command::Hypothesis(command) => match command.command {
+ HypothesisSubcommand::Add(args) => run_quick_hypothesis(args),
+ },
Command::Tag { command } => match command {
TagCommand::Add(args) => run_tag_add(args),
TagCommand::List(project) => run_tag_list(project),
},
- Command::Research(command) => match command.command {
- ResearchSubcommand::Add(args) => run_quick_research(args),
+ Command::Source(command) => match command.command {
+ SourceSubcommand::Add(args) => run_quick_source(args),
},
Command::Metric { command } => match command {
MetricCommand::Define(args) => run_metric_define(args),
@@ -731,7 +794,9 @@ fn run() -> Result<(), StoreError> {
DimensionCommand::List(project) => run_dimension_list(project),
},
Command::Experiment { command } => match command {
- ExperimentCommand::Close(args) => run_experiment_close(args),
+ ExperimentCommand::Open(args) => run_experiment_open(args),
+ ExperimentCommand::List(args) => run_experiment_list(args),
+ ExperimentCommand::Close(args) => run_experiment_close(*args),
},
Command::Mcp { command } => match command {
McpCommand::Serve(args) => mcp::serve(args.project),
@@ -942,6 +1007,25 @@ fn run_quick_note(args: QuickNoteArgs) -> Result<(), StoreError> {
print_json(&node)
}
+fn run_quick_hypothesis(args: QuickHypothesisArgs) -> Result<(), StoreError> {
+ let mut store = open_store(&args.project.project)?;
+ let payload = NodePayload::with_schema(
+ store.schema().schema_ref(),
+ json_object(json!({ "body": args.body }))?,
+ );
+ let node = store.add_node(CreateNodeRequest {
+ class: NodeClass::Hypothesis,
+ frontier_id: Some(parse_frontier_id(&args.frontier)?),
+ title: NonEmptyText::new(args.title)?,
+ summary: Some(NonEmptyText::new(args.summary)?),
+ tags: None,
+ payload,
+ annotations: Vec::new(),
+ attachments: lineage_attachments(args.parents)?,
+ })?;
+ print_json(&node)
+}
+
fn run_tag_add(args: TagAddArgs) -> Result<(), StoreError> {
let mut store = open_store(&args.project.project)?;
let tag = store.add_tag(
@@ -956,14 +1040,14 @@ fn run_tag_list(args: ProjectArg) -> Result<(), StoreError> {
print_json(&store.list_tags()?)
}
-fn run_quick_research(args: QuickResearchArgs) -> Result<(), StoreError> {
+fn run_quick_source(args: QuickSourceArgs) -> Result<(), StoreError> {
let mut store = open_store(&args.project.project)?;
let payload = NodePayload::with_schema(
store.schema().schema_ref(),
json_object(json!({ "body": args.body }))?,
);
let node = store.add_node(CreateNodeRequest {
- class: NodeClass::Research,
+ class: NodeClass::Source,
frontier_id: args
.frontier
.as_deref()
@@ -1042,9 +1126,31 @@ fn run_dimension_list(args: ProjectArg) -> Result<(), StoreError> {
print_json(&store.list_run_dimensions()?)
}
+fn run_experiment_open(args: ExperimentOpenArgs) -> Result<(), StoreError> {
+ let mut store = open_store(&args.project.project)?;
+ let summary = args.summary.map(NonEmptyText::new).transpose()?;
+ let experiment = store.open_experiment(OpenExperimentRequest {
+ frontier_id: parse_frontier_id(&args.frontier)?,
+ base_checkpoint_id: parse_checkpoint_id(&args.base_checkpoint)?,
+ hypothesis_node_id: parse_node_id(&args.hypothesis_node)?,
+ title: NonEmptyText::new(args.title)?,
+ summary,
+ })?;
+ print_json(&experiment)
+}
+
+fn run_experiment_list(args: ExperimentListArgs) -> Result<(), StoreError> {
+ let store = open_store(&args.project.project)?;
+ let frontier_id = args
+ .frontier
+ .as_deref()
+ .map(parse_frontier_id)
+ .transpose()?;
+ print_json(&store.list_open_experiments(frontier_id)?)
+}
+
fn run_experiment_close(args: ExperimentCloseArgs) -> Result<(), StoreError> {
let mut store = open_store(&args.project.project)?;
- let frontier_id = parse_frontier_id(&args.frontier)?;
let snapshot = store
.auto_capture_checkpoint(NonEmptyText::new(args.candidate_summary.clone())?)?
.map(|seed| seed.snapshot)
@@ -1058,10 +1164,28 @@ fn run_experiment_close(args: ExperimentCloseArgs) -> Result<(), StoreError> {
to_text_vec(args.argv)?,
parse_env(args.env),
)?;
+ let analysis = match (
+ args.analysis_title,
+ args.analysis_summary,
+ args.analysis_body,
+ ) {
+ (Some(title), Some(summary), Some(body)) => Some(ExperimentAnalysisDraft {
+ title: NonEmptyText::new(title)?,
+ summary: NonEmptyText::new(summary)?,
+ body: NonEmptyText::new(body)?,
+ }),
+ (None, None, None) => None,
+ _ => {
+ return Err(StoreError::Json(serde_json::Error::io(
+ std::io::Error::new(
+ std::io::ErrorKind::InvalidInput,
+ "analysis-title, analysis-summary, and analysis-body must be provided together",
+ ),
+ )));
+ }
+ };
let receipt = store.close_experiment(CloseExperimentRequest {
- frontier_id,
- base_checkpoint_id: parse_checkpoint_id(&args.base_checkpoint)?,
- change_node_id: parse_node_id(&args.change_node)?,
+ experiment_id: parse_experiment_id(&args.experiment_id)?,
candidate_summary: NonEmptyText::new(args.candidate_summary)?,
candidate_snapshot: snapshot,
run_title: NonEmptyText::new(args.run_title)?,
@@ -1081,9 +1205,9 @@ fn run_experiment_close(args: ExperimentCloseArgs) -> Result<(), StoreError> {
next_hypotheses: to_text_vec(args.next_hypotheses)?,
},
verdict: args.verdict.into(),
+ analysis,
decision_title: NonEmptyText::new(args.decision_title)?,
decision_rationale: NonEmptyText::new(args.decision_rationale)?,
- analysis_node_id: None,
})?;
print_json(&receipt)
}
@@ -1378,7 +1502,7 @@ fn validate_cli_prose_payload(
summary: Option<&str>,
payload: &NodePayload,
) -> Result<(), StoreError> {
- if !matches!(class, NodeClass::Note | NodeClass::Research) {
+ if !matches!(class, NodeClass::Note | NodeClass::Source) {
return Ok(());
}
if summary.is_none() {
@@ -1584,6 +1708,12 @@ fn parse_checkpoint_id(raw: &str) -> Result<fidget_spinner_core::CheckpointId, S
))
}
+fn parse_experiment_id(raw: &str) -> Result<fidget_spinner_core::ExperimentId, StoreError> {
+ Ok(fidget_spinner_core::ExperimentId::from_uuid(
+ Uuid::parse_str(raw)?,
+ ))
+}
+
fn print_json<T: Serialize>(value: &T) -> Result<(), StoreError> {
println!("{}", to_pretty_json(value)?);
Ok(())
@@ -1604,12 +1734,11 @@ impl From<CliNodeClass> for NodeClass {
fn from(value: CliNodeClass) -> Self {
match value {
CliNodeClass::Contract => Self::Contract,
- CliNodeClass::Change => Self::Change,
+ CliNodeClass::Hypothesis => Self::Hypothesis,
CliNodeClass::Run => Self::Run,
CliNodeClass::Analysis => Self::Analysis,
CliNodeClass::Decision => Self::Decision,
- CliNodeClass::Research => Self::Research,
- CliNodeClass::Enabling => Self::Enabling,
+ CliNodeClass::Source => Self::Source,
CliNodeClass::Note => Self::Note,
}
}
@@ -1651,7 +1780,7 @@ impl From<CliMetricSource> for MetricFieldSource {
fn from(value: CliMetricSource) -> Self {
match value {
CliMetricSource::RunMetric => Self::RunMetric,
- CliMetricSource::ChangePayload => Self::ChangePayload,
+ CliMetricSource::HypothesisPayload => Self::HypothesisPayload,
CliMetricSource::RunPayload => Self::RunPayload,
CliMetricSource::AnalysisPayload => Self::AnalysisPayload,
CliMetricSource::DecisionPayload => Self::DecisionPayload,
diff --git a/crates/fidget-spinner-cli/src/mcp/catalog.rs b/crates/fidget-spinner-cli/src/mcp/catalog.rs
index 0831ba4..3b8abcc 100644
--- a/crates/fidget-spinner-cli/src/mcp/catalog.rs
+++ b/crates/fidget-spinner-cli/src/mcp/catalog.rs
@@ -115,9 +115,9 @@ pub(crate) fn tool_spec(name: &str) -> Option<ToolSpec> {
dispatch: DispatchTarget::Worker,
replay: ReplayContract::NeverReplay,
}),
- "change.record" => Some(ToolSpec {
- name: "change.record",
- description: "Record a core-path change hypothesis with low ceremony.",
+ "hypothesis.record" => Some(ToolSpec {
+ name: "hypothesis.record",
+ description: "Record a core-path hypothesis with low ceremony.",
dispatch: DispatchTarget::Worker,
replay: ReplayContract::NeverReplay,
}),
@@ -151,9 +151,9 @@ pub(crate) fn tool_spec(name: &str) -> Option<ToolSpec> {
dispatch: DispatchTarget::Worker,
replay: ReplayContract::NeverReplay,
}),
- "research.record" => Some(ToolSpec {
- name: "research.record",
- description: "Record off-path research or enabling work that should live in the DAG but not on the bureaucratic core path.",
+ "source.record" => Some(ToolSpec {
+ name: "source.record",
+ description: "Record imported sources and documentary context that should live in the DAG without polluting the core path.",
dispatch: DispatchTarget::Worker,
replay: ReplayContract::NeverReplay,
}),
@@ -193,9 +193,27 @@ pub(crate) fn tool_spec(name: &str) -> Option<ToolSpec> {
dispatch: DispatchTarget::Worker,
replay: ReplayContract::NeverReplay,
}),
+ "experiment.open" => Some(ToolSpec {
+ name: "experiment.open",
+ description: "Open a stateful experiment against one hypothesis and one base checkpoint.",
+ dispatch: DispatchTarget::Worker,
+ replay: ReplayContract::NeverReplay,
+ }),
+ "experiment.list" => Some(ToolSpec {
+ name: "experiment.list",
+ description: "List currently open experiments, optionally narrowed to one frontier.",
+ dispatch: DispatchTarget::Worker,
+ replay: ReplayContract::Convergent,
+ }),
+ "experiment.read" => Some(ToolSpec {
+ name: "experiment.read",
+ description: "Read one currently open experiment by id.",
+ dispatch: DispatchTarget::Worker,
+ replay: ReplayContract::Convergent,
+ }),
"experiment.close" => Some(ToolSpec {
name: "experiment.close",
- description: "Atomically close a core-path experiment with typed run dimensions, preregistered metric observations, candidate checkpoint capture, note, and verdict.",
+ description: "Close one open experiment with typed run dimensions, preregistered metric observations, candidate checkpoint capture, optional analysis, note, and verdict.",
dispatch: DispatchTarget::Worker,
replay: ReplayContract::NeverReplay,
}),
@@ -268,19 +286,22 @@ pub(crate) fn tool_definitions() -> Vec<Value> {
"frontier.status",
"frontier.init",
"node.create",
- "change.record",
+ "hypothesis.record",
"node.list",
"node.read",
"node.annotate",
"node.archive",
"note.quick",
- "research.record",
+ "source.record",
"metric.define",
"run.dimension.define",
"run.dimension.list",
"metric.keys",
"metric.best",
"metric.migrate",
+ "experiment.open",
+ "experiment.list",
+ "experiment.read",
"experiment.close",
"skill.list",
"skill.show",
@@ -414,29 +435,26 @@ fn input_schema(name: &str) -> Value {
"class": node_class_schema(),
"frontier_id": { "type": "string" },
"title": { "type": "string" },
- "summary": { "type": "string", "description": "Required for `note` and `research` nodes." },
+ "summary": { "type": "string", "description": "Required for `note` and `source` nodes." },
"tags": { "type": "array", "items": tag_name_schema(), "description": "Required for `note` nodes; optional for other classes." },
- "payload": { "type": "object", "description": "`note` and `research` nodes require a non-empty string `body` field." },
+ "payload": { "type": "object", "description": "`note` and `source` nodes require a non-empty string `body` field." },
"annotations": { "type": "array", "items": annotation_schema() },
"parents": { "type": "array", "items": { "type": "string" } }
},
"required": ["class", "title"],
"additionalProperties": false
}),
- "change.record" => json!({
+ "hypothesis.record" => json!({
"type": "object",
"properties": {
"frontier_id": { "type": "string" },
"title": { "type": "string" },
"summary": { "type": "string" },
"body": { "type": "string" },
- "hypothesis": { "type": "string" },
- "base_checkpoint_id": { "type": "string" },
- "benchmark_suite": { "type": "string" },
"annotations": { "type": "array", "items": annotation_schema() },
"parents": { "type": "array", "items": { "type": "string" } }
},
- "required": ["frontier_id", "title", "body"],
+ "required": ["frontier_id", "title", "summary", "body"],
"additionalProperties": false
}),
"node.list" => json!({
@@ -483,7 +501,7 @@ fn input_schema(name: &str) -> Value {
"required": ["title", "summary", "body", "tags"],
"additionalProperties": false
}),
- "research.record" => json!({
+ "source.record" => json!({
"type": "object",
"properties": {
"frontier_id": { "type": "string" },
@@ -540,12 +558,37 @@ fn input_schema(name: &str) -> Value {
"required": ["key"],
"additionalProperties": false
}),
- "experiment.close" => json!({
+ "experiment.open" => json!({
"type": "object",
"properties": {
"frontier_id": { "type": "string" },
"base_checkpoint_id": { "type": "string" },
- "change_node_id": { "type": "string" },
+ "hypothesis_node_id": { "type": "string" },
+ "title": { "type": "string" },
+ "summary": { "type": "string" }
+ },
+ "required": ["frontier_id", "base_checkpoint_id", "hypothesis_node_id", "title"],
+ "additionalProperties": false
+ }),
+ "experiment.list" => json!({
+ "type": "object",
+ "properties": {
+ "frontier_id": { "type": "string" }
+ },
+ "additionalProperties": false
+ }),
+ "experiment.read" => json!({
+ "type": "object",
+ "properties": {
+ "experiment_id": { "type": "string" }
+ },
+ "required": ["experiment_id"],
+ "additionalProperties": false
+ }),
+ "experiment.close" => json!({
+ "type": "object",
+ "properties": {
+ "experiment_id": { "type": "string" },
"candidate_summary": { "type": "string" },
"run": run_schema(),
"primary_metric": metric_value_schema(),
@@ -554,12 +597,10 @@ fn input_schema(name: &str) -> Value {
"verdict": verdict_schema(),
"decision_title": { "type": "string" },
"decision_rationale": { "type": "string" },
- "analysis_node_id": { "type": "string" }
+ "analysis": analysis_schema()
},
"required": [
- "frontier_id",
- "base_checkpoint_id",
- "change_node_id",
+ "experiment_id",
"candidate_summary",
"run",
"primary_metric",
@@ -612,6 +653,19 @@ fn annotation_schema() -> Value {
})
}
+fn analysis_schema() -> Value {
+ json!({
+ "type": "object",
+ "properties": {
+ "title": { "type": "string" },
+ "summary": { "type": "string" },
+ "body": { "type": "string" }
+ },
+ "required": ["title", "summary", "body"],
+ "additionalProperties": false
+ })
+}
+
fn tag_name_schema() -> Value {
json!({
"type": "string",
@@ -622,7 +676,7 @@ fn tag_name_schema() -> Value {
fn node_class_schema() -> Value {
json!({
"type": "string",
- "enum": ["contract", "change", "run", "analysis", "decision", "research", "enabling", "note"]
+ "enum": ["contract", "hypothesis", "run", "analysis", "decision", "source", "note"]
})
}
@@ -638,7 +692,7 @@ fn metric_source_schema() -> Value {
"type": "string",
"enum": [
"run_metric",
- "change_payload",
+ "hypothesis_payload",
"run_payload",
"analysis_payload",
"decision_payload"
diff --git a/crates/fidget-spinner-cli/src/mcp/service.rs b/crates/fidget-spinner-cli/src/mcp/service.rs
index 62e3641..05f2382 100644
--- a/crates/fidget-spinner-cli/src/mcp/service.rs
+++ b/crates/fidget-spinner-cli/src/mcp/service.rs
@@ -11,10 +11,10 @@ use fidget_spinner_core::{
};
use fidget_spinner_store_sqlite::{
CloseExperimentRequest, CreateFrontierRequest, CreateNodeRequest, DefineMetricRequest,
- DefineRunDimensionRequest, EdgeAttachment, EdgeAttachmentDirection, ExperimentReceipt,
- ListNodesQuery, MetricBestQuery, MetricFieldSource, MetricKeyQuery, MetricKeySummary,
- MetricRankOrder, NodeSummary, ProjectStore, RemoveSchemaFieldRequest, StoreError,
- UpsertSchemaFieldRequest,
+ DefineRunDimensionRequest, EdgeAttachment, EdgeAttachmentDirection, ExperimentAnalysisDraft,
+ ExperimentReceipt, ListNodesQuery, MetricBestQuery, MetricFieldSource, MetricKeyQuery,
+ MetricKeySummary, MetricRankOrder, NodeSummary, OpenExperimentRequest, OpenExperimentSummary,
+ ProjectStore, RemoveSchemaFieldRequest, StoreError, UpsertSchemaFieldRequest,
};
use serde::Deserialize;
use serde_json::{Map, Value, json};
@@ -303,51 +303,43 @@ impl WorkerService {
"tools/call:node.create",
)
}
- "change.record" => {
- let args = deserialize::<ChangeRecordToolArgs>(arguments)?;
- let mut fields = Map::new();
- let _ = fields.insert("body".to_owned(), Value::String(args.body));
- if let Some(hypothesis) = args.hypothesis {
- let _ = fields.insert("hypothesis".to_owned(), Value::String(hypothesis));
- }
- if let Some(base_checkpoint_id) = args.base_checkpoint_id {
- let _ = fields.insert(
- "base_checkpoint_id".to_owned(),
- Value::String(base_checkpoint_id),
- );
- }
- if let Some(benchmark_suite) = args.benchmark_suite {
- let _ =
- fields.insert("benchmark_suite".to_owned(), Value::String(benchmark_suite));
- }
+ "hypothesis.record" => {
+ let args = deserialize::<HypothesisRecordToolArgs>(arguments)?;
let node = self
.store
.add_node(CreateNodeRequest {
- class: NodeClass::Change,
+ class: NodeClass::Hypothesis,
frontier_id: Some(
crate::parse_frontier_id(&args.frontier_id)
- .map_err(store_fault("tools/call:change.record"))?,
+ .map_err(store_fault("tools/call:hypothesis.record"))?,
),
title: NonEmptyText::new(args.title)
- .map_err(store_fault("tools/call:change.record"))?,
- summary: args
- .summary
- .map(NonEmptyText::new)
- .transpose()
- .map_err(store_fault("tools/call:change.record"))?,
+ .map_err(store_fault("tools/call:hypothesis.record"))?,
+ summary: Some(
+ NonEmptyText::new(args.summary)
+ .map_err(store_fault("tools/call:hypothesis.record"))?,
+ ),
tags: None,
- payload: NodePayload::with_schema(self.store.schema().schema_ref(), fields),
+ payload: NodePayload::with_schema(
+ self.store.schema().schema_ref(),
+ crate::json_object(json!({ "body": args.body }))
+ .map_err(store_fault("tools/call:hypothesis.record"))?,
+ ),
annotations: tool_annotations(args.annotations)
- .map_err(store_fault("tools/call:change.record"))?,
+ .map_err(store_fault("tools/call:hypothesis.record"))?,
attachments: lineage_attachments(args.parents)
- .map_err(store_fault("tools/call:change.record"))?,
+ .map_err(store_fault("tools/call:hypothesis.record"))?,
})
- .map_err(store_fault("tools/call:change.record"))?;
+ .map_err(store_fault("tools/call:hypothesis.record"))?;
tool_success(
- created_node_output("recorded change", &node, "tools/call:change.record")?,
+ created_node_output(
+ "recorded hypothesis",
+ &node,
+ "tools/call:hypothesis.record",
+ )?,
presentation,
FaultStage::Worker,
- "tools/call:change.record",
+ "tools/call:hypothesis.record",
)
}
"node.list" => {
@@ -498,44 +490,45 @@ impl WorkerService {
"tools/call:note.quick",
)
}
- "research.record" => {
- let args = deserialize::<ResearchRecordToolArgs>(arguments)?;
+ "source.record" => {
+ let args = deserialize::<SourceRecordToolArgs>(arguments)?;
let node = self
.store
.add_node(CreateNodeRequest {
- class: NodeClass::Research,
+ class: NodeClass::Source,
frontier_id: args
.frontier_id
.as_deref()
.map(crate::parse_frontier_id)
.transpose()
- .map_err(store_fault("tools/call:research.record"))?,
+ .map_err(store_fault("tools/call:source.record"))?,
title: NonEmptyText::new(args.title)
- .map_err(store_fault("tools/call:research.record"))?,
+ .map_err(store_fault("tools/call:source.record"))?,
summary: Some(
NonEmptyText::new(args.summary)
- .map_err(store_fault("tools/call:research.record"))?,
- ),
- tags: Some(
- parse_tag_set(args.tags)
- .map_err(store_fault("tools/call:research.record"))?,
+ .map_err(store_fault("tools/call:source.record"))?,
),
+ tags: args
+ .tags
+ .map(parse_tag_set)
+ .transpose()
+ .map_err(store_fault("tools/call:source.record"))?,
payload: NodePayload::with_schema(
self.store.schema().schema_ref(),
crate::json_object(json!({ "body": args.body }))
- .map_err(store_fault("tools/call:research.record"))?,
+ .map_err(store_fault("tools/call:source.record"))?,
),
annotations: tool_annotations(args.annotations)
- .map_err(store_fault("tools/call:research.record"))?,
+ .map_err(store_fault("tools/call:source.record"))?,
attachments: lineage_attachments(args.parents)
- .map_err(store_fault("tools/call:research.record"))?,
+ .map_err(store_fault("tools/call:source.record"))?,
})
- .map_err(store_fault("tools/call:research.record"))?;
+ .map_err(store_fault("tools/call:source.record"))?;
tool_success(
- created_node_output("recorded research", &node, "tools/call:research.record")?,
+ created_node_output("recorded source", &node, "tools/call:source.record")?,
presentation,
FaultStage::Worker,
- "tools/call:research.record",
+ "tools/call:source.record",
)
}
"metric.define" => {
@@ -702,10 +695,74 @@ impl WorkerService {
"tools/call:metric.migrate",
)
}
+ "experiment.open" => {
+ let args = deserialize::<ExperimentOpenToolArgs>(arguments)?;
+ let item = self
+ .store
+ .open_experiment(OpenExperimentRequest {
+ frontier_id: crate::parse_frontier_id(&args.frontier_id)
+ .map_err(store_fault("tools/call:experiment.open"))?,
+ base_checkpoint_id: crate::parse_checkpoint_id(&args.base_checkpoint_id)
+ .map_err(store_fault("tools/call:experiment.open"))?,
+ hypothesis_node_id: crate::parse_node_id(&args.hypothesis_node_id)
+ .map_err(store_fault("tools/call:experiment.open"))?,
+ title: NonEmptyText::new(args.title)
+ .map_err(store_fault("tools/call:experiment.open"))?,
+ summary: args
+ .summary
+ .map(NonEmptyText::new)
+ .transpose()
+ .map_err(store_fault("tools/call:experiment.open"))?,
+ })
+ .map_err(store_fault("tools/call:experiment.open"))?;
+ tool_success(
+ experiment_open_output(
+ &item,
+ "tools/call:experiment.open",
+ "opened experiment",
+ )?,
+ presentation,
+ FaultStage::Worker,
+ "tools/call:experiment.open",
+ )
+ }
+ "experiment.list" => {
+ let args = deserialize::<ExperimentListToolArgs>(arguments)?;
+ let items = self
+ .store
+ .list_open_experiments(
+ args.frontier_id
+ .as_deref()
+ .map(crate::parse_frontier_id)
+ .transpose()
+ .map_err(store_fault("tools/call:experiment.list"))?,
+ )
+ .map_err(store_fault("tools/call:experiment.list"))?;
+ tool_success(
+ experiment_list_output(items.as_slice())?,
+ presentation,
+ FaultStage::Worker,
+ "tools/call:experiment.list",
+ )
+ }
+ "experiment.read" => {
+ let args = deserialize::<ExperimentReadToolArgs>(arguments)?;
+ let item = self
+ .store
+ .read_open_experiment(
+ crate::parse_experiment_id(&args.experiment_id)
+ .map_err(store_fault("tools/call:experiment.read"))?,
+ )
+ .map_err(store_fault("tools/call:experiment.read"))?;
+ tool_success(
+ experiment_open_output(&item, "tools/call:experiment.read", "open experiment")?,
+ presentation,
+ FaultStage::Worker,
+ "tools/call:experiment.read",
+ )
+ }
"experiment.close" => {
let args = deserialize::<ExperimentCloseToolArgs>(arguments)?;
- let frontier_id = crate::parse_frontier_id(&args.frontier_id)
- .map_err(store_fault("tools/call:experiment.close"))?;
let snapshot = self
.store
.auto_capture_checkpoint(
@@ -728,10 +785,7 @@ impl WorkerService {
let receipt = self
.store
.close_experiment(CloseExperimentRequest {
- frontier_id,
- base_checkpoint_id: crate::parse_checkpoint_id(&args.base_checkpoint_id)
- .map_err(store_fault("tools/call:experiment.close"))?,
- change_node_id: crate::parse_node_id(&args.change_node_id)
+ experiment_id: crate::parse_experiment_id(&args.experiment_id)
.map_err(store_fault("tools/call:experiment.close"))?,
candidate_summary: NonEmptyText::new(args.candidate_summary)
.map_err(store_fault("tools/call:experiment.close"))?,
@@ -776,16 +830,15 @@ impl WorkerService {
},
verdict: parse_verdict_name(&args.verdict)
.map_err(store_fault("tools/call:experiment.close"))?,
+ analysis: args
+ .analysis
+ .map(experiment_analysis_from_wire)
+ .transpose()
+ .map_err(store_fault("tools/call:experiment.close"))?,
decision_title: NonEmptyText::new(args.decision_title)
.map_err(store_fault("tools/call:experiment.close"))?,
decision_rationale: NonEmptyText::new(args.decision_rationale)
.map_err(store_fault("tools/call:experiment.close"))?,
- analysis_node_id: args
- .analysis_node_id
- .as_deref()
- .map(crate::parse_node_id)
- .transpose()
- .map_err(store_fault("tools/call:experiment.close"))?,
})
.map_err(store_fault("tools/call:experiment.close"))?;
tool_success(
@@ -1296,6 +1349,7 @@ fn experiment_close_output(
"candidate_checkpoint_id": receipt.experiment.candidate_checkpoint_id,
"verdict": format!("{:?}", receipt.experiment.verdict).to_ascii_lowercase(),
"run_id": receipt.run.run_id,
+ "hypothesis_node_id": receipt.experiment.hypothesis_node_id,
"decision_node_id": receipt.decision_node.id,
"dimensions": run_dimensions_value(&receipt.experiment.result.dimensions),
"primary_metric": metric_value(store, &receipt.experiment.result.primary_metric)?,
@@ -1308,6 +1362,7 @@ fn experiment_close_output(
"closed experiment {} on frontier {}",
receipt.experiment.id, receipt.experiment.frontier_id
),
+ format!("hypothesis: {}", receipt.experiment.hypothesis_node_id),
format!("candidate: {}", receipt.experiment.candidate_checkpoint_id),
format!(
"verdict: {}",
@@ -1330,6 +1385,71 @@ fn experiment_close_output(
)
}
+fn experiment_open_output(
+ item: &OpenExperimentSummary,
+ operation: &'static str,
+ action: &'static str,
+) -> Result<ToolOutput, FaultRecord> {
+ let concise = json!({
+ "experiment_id": item.id,
+ "frontier_id": item.frontier_id,
+ "base_checkpoint_id": item.base_checkpoint_id,
+ "hypothesis_node_id": item.hypothesis_node_id,
+ "title": item.title,
+ "summary": item.summary,
+ });
+ detailed_tool_output(
+ &concise,
+ item,
+ [
+ format!("{action} {}", item.id),
+ format!("frontier: {}", item.frontier_id),
+ format!("hypothesis: {}", item.hypothesis_node_id),
+ format!("base checkpoint: {}", item.base_checkpoint_id),
+ format!("title: {}", item.title),
+ item.summary
+ .as_ref()
+ .map(|summary| format!("summary: {summary}"))
+ .unwrap_or_else(|| "summary: <none>".to_owned()),
+ ]
+ .join("\n"),
+ None,
+ FaultStage::Worker,
+ operation,
+ )
+}
+
+fn experiment_list_output(items: &[OpenExperimentSummary]) -> Result<ToolOutput, FaultRecord> {
+ let concise = items
+ .iter()
+ .map(|item| {
+ json!({
+ "experiment_id": item.id,
+ "frontier_id": item.frontier_id,
+ "base_checkpoint_id": item.base_checkpoint_id,
+ "hypothesis_node_id": item.hypothesis_node_id,
+ "title": item.title,
+ "summary": item.summary,
+ })
+ })
+ .collect::<Vec<_>>();
+ let mut lines = vec![format!("{} open experiment(s)", items.len())];
+ lines.extend(items.iter().map(|item| {
+ format!(
+ "{} {} | hypothesis={} | checkpoint={}",
+ item.id, item.title, item.hypothesis_node_id, item.base_checkpoint_id,
+ )
+ }));
+ detailed_tool_output(
+ &concise,
+ &items,
+ lines.join("\n"),
+ None,
+ FaultStage::Worker,
+ "tools/call:experiment.list",
+ )
+}
+
fn metric_keys_output(keys: &[MetricKeySummary]) -> Result<ToolOutput, FaultRecord> {
let concise = keys
.iter()
@@ -1392,8 +1512,8 @@ fn metric_best_output(
"order": item.order.as_str(),
"experiment_id": item.experiment_id,
"frontier_id": item.frontier_id,
- "change_node_id": item.change_node_id,
- "change_title": item.change_title,
+ "hypothesis_node_id": item.hypothesis_node_id,
+ "hypothesis_title": item.hypothesis_title,
"verdict": metric_verdict_name(item.verdict),
"candidate_checkpoint_id": item.candidate_checkpoint_id,
"candidate_commit_hash": item.candidate_commit_hash,
@@ -1412,7 +1532,7 @@ fn metric_best_output(
item.key,
item.value,
item.source.as_str(),
- item.change_title,
+ item.hypothesis_title,
metric_verdict_name(item.verdict),
item.candidate_commit_hash,
item.candidate_checkpoint_id,
@@ -1775,7 +1895,7 @@ fn filtered_payload_fields(
fields: &Map<String, Value>,
) -> impl Iterator<Item = (&String, &Value)> + '_ {
fields.iter().filter(move |(name, _)| {
- !matches!(class, NodeClass::Note | NodeClass::Research) || name.as_str() != "body"
+ !matches!(class, NodeClass::Note | NodeClass::Source) || name.as_str() != "body"
})
}
@@ -1817,7 +1937,7 @@ fn payload_value_preview(value: &Value) -> Value {
}
fn is_prose_node(class: NodeClass) -> bool {
- matches!(class, NodeClass::Note | NodeClass::Research)
+ matches!(class, NodeClass::Note | NodeClass::Source)
}
fn truncated_inline_preview(text: &str, limit: usize) -> String {
@@ -2017,6 +2137,14 @@ fn metric_value_from_wire(raw: WireMetricValue) -> Result<MetricValue, StoreErro
})
}
+fn experiment_analysis_from_wire(raw: WireAnalysis) -> Result<ExperimentAnalysisDraft, StoreError> {
+ Ok(ExperimentAnalysisDraft {
+ title: NonEmptyText::new(raw.title)?,
+ summary: NonEmptyText::new(raw.summary)?,
+ body: NonEmptyText::new(raw.body)?,
+ })
+}
+
fn metric_definition(store: &ProjectStore, key: &NonEmptyText) -> Result<MetricSpec, FaultRecord> {
store
.list_metric_definitions()
@@ -2071,12 +2199,11 @@ fn capture_code_snapshot(project_root: &Utf8Path) -> Result<CodeSnapshotRef, Sto
fn parse_node_class_name(raw: &str) -> Result<NodeClass, StoreError> {
match raw {
"contract" => Ok(NodeClass::Contract),
- "change" => Ok(NodeClass::Change),
+ "hypothesis" => Ok(NodeClass::Hypothesis),
"run" => Ok(NodeClass::Run),
"analysis" => Ok(NodeClass::Analysis),
"decision" => Ok(NodeClass::Decision),
- "research" => Ok(NodeClass::Research),
- "enabling" => Ok(NodeClass::Enabling),
+ "source" => Ok(NodeClass::Source),
"note" => Ok(NodeClass::Note),
other => Err(crate::invalid_input(format!(
"unknown node class `{other}`"
@@ -2091,7 +2218,7 @@ fn parse_metric_unit_name(raw: &str) -> Result<MetricUnit, StoreError> {
fn parse_metric_source_name(raw: &str) -> Result<MetricFieldSource, StoreError> {
match raw {
"run_metric" => Ok(MetricFieldSource::RunMetric),
- "change_payload" => Ok(MetricFieldSource::ChangePayload),
+ "hypothesis_payload" => Ok(MetricFieldSource::HypothesisPayload),
"run_payload" => Ok(MetricFieldSource::RunPayload),
"analysis_payload" => Ok(MetricFieldSource::AnalysisPayload),
"decision_payload" => Ok(MetricFieldSource::DecisionPayload),
@@ -2234,14 +2361,11 @@ struct NodeCreateToolArgs {
}
#[derive(Debug, Deserialize)]
-struct ChangeRecordToolArgs {
+struct HypothesisRecordToolArgs {
frontier_id: String,
title: String,
- summary: Option<String>,
+ summary: String,
body: String,
- hypothesis: Option<String>,
- base_checkpoint_id: Option<String>,
- benchmark_suite: Option<String>,
#[serde(default)]
annotations: Vec<WireAnnotation>,
#[serde(default)]
@@ -2292,13 +2416,12 @@ struct QuickNoteToolArgs {
}
#[derive(Debug, Deserialize)]
-struct ResearchRecordToolArgs {
+struct SourceRecordToolArgs {
frontier_id: Option<String>,
title: String,
summary: String,
body: String,
- #[serde(default)]
- tags: Vec<String>,
+ tags: Option<Vec<String>>,
#[serde(default)]
annotations: Vec<WireAnnotation>,
#[serde(default)]
@@ -2355,10 +2478,27 @@ struct MetricBestToolArgs {
}
#[derive(Debug, Deserialize)]
-struct ExperimentCloseToolArgs {
+struct ExperimentOpenToolArgs {
frontier_id: String,
base_checkpoint_id: String,
- change_node_id: String,
+ hypothesis_node_id: String,
+ title: String,
+ summary: Option<String>,
+}
+
+#[derive(Debug, Deserialize, Default)]
+struct ExperimentListToolArgs {
+ frontier_id: Option<String>,
+}
+
+#[derive(Debug, Deserialize)]
+struct ExperimentReadToolArgs {
+ experiment_id: String,
+}
+
+#[derive(Debug, Deserialize)]
+struct ExperimentCloseToolArgs {
+ experiment_id: String,
candidate_summary: String,
run: WireRun,
primary_metric: WireMetricValue,
@@ -2368,7 +2508,7 @@ struct ExperimentCloseToolArgs {
verdict: String,
decision_title: String,
decision_rationale: String,
- analysis_node_id: Option<String>,
+ analysis: Option<WireAnalysis>,
}
#[derive(Debug, Deserialize)]
@@ -2403,6 +2543,13 @@ struct WireRun {
}
#[derive(Debug, Deserialize)]
+struct WireAnalysis {
+ title: String,
+ summary: String,
+ body: String,
+}
+
+#[derive(Debug, Deserialize)]
struct WireRunCommand {
working_directory: Option<String>,
argv: Vec<String>,