mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
refactor(accumulator): the response IS the artifact — free-form output, transparent runs
The artifact model now matches the mental model: instructions + context → model → response, persisted verbatim. Any shape the task asks for. - Delete schema.rs and validate.rs entirely: no output contract, no splice, no refusal path. complete_run is infallible; Error::Nonconforming and RunOutcome::Refused are gone. Provenance (shown_ids, coverage, truncated) stays engine-computed from the plan, never parsed from output. - spec: schema becomes a provenance label (freeform@v1 default; old chains keep channel-digest@v1, no migration). New DEFAULT_INSTRUCTIONS. - run: model_input = TASK (user instructions) + short engine guidance (context explained, citations render as links) + CONTEXT; prior version rides as '--- PRIOR VERSION (name vN) ---'. - preflight gains include_input: the Ready plan can carry the exact string the model would receive — the behind-the-curtain seam. - UI: fold cards always show their instructions; the plan box explains incremental coverage and offers the exact model input behind a disclosure; artifact header explains what 'covers' means (this run's new events) and that versions build on the prior; refused branch removed. Gates: cargo test -p buzz-accumulator 67/67, clippy -D warnings, fmt; ui tsc --noEmit + vite build clean. Live-verified on scratch daemon :4641 (preflight include_input round-trip; legacy channel-digest@v1 artifact still loads). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Riley Crane <rileycrane@squareup.com>
This commit is contained in:
@@ -28,14 +28,6 @@ pub enum RunOutcome {
|
||||
/// New (uncovered) signals waiting in the window.
|
||||
pending: usize,
|
||||
},
|
||||
/// Model output broke the artifact contract; nothing persisted.
|
||||
/// The paid-for output is salvaged here rather than discarded.
|
||||
Refused {
|
||||
/// The validator's refusal.
|
||||
reason: String,
|
||||
/// Raw model output for inspection.
|
||||
model_output: String,
|
||||
},
|
||||
/// A concurrent run appended this version first; nothing persisted.
|
||||
Unpublished {
|
||||
/// What happened.
|
||||
@@ -83,6 +75,11 @@ pub enum PreflightOutcome {
|
||||
estimate: crate::estimate::Estimate,
|
||||
/// Window actually queried, `[since, until_exclusive)`.
|
||||
window: (i64, i64),
|
||||
/// The exact string the model would receive — the transparency seam
|
||||
/// ("what's happening behind the curtain"). Present only when the
|
||||
/// caller asked for it.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
model_input: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -164,12 +161,14 @@ async fn plan(
|
||||
}
|
||||
|
||||
/// Zero-spend preflight: what would a run over this window do, and at what
|
||||
/// estimated cost?
|
||||
/// estimated cost? With `include_input`, the Ready outcome carries the exact
|
||||
/// string the model would receive.
|
||||
pub async fn preflight(
|
||||
store: &Store,
|
||||
name: &str,
|
||||
since: i64,
|
||||
until_exclusive: i64,
|
||||
include_input: bool,
|
||||
) -> Result<PreflightOutcome, FoldError> {
|
||||
let (_, _, plan) = plan(store, name, since, until_exclusive).await?;
|
||||
Ok(match plan {
|
||||
@@ -181,6 +180,7 @@ pub async fn preflight(
|
||||
truncated: rp.truncated,
|
||||
estimate: rp.estimate,
|
||||
window: (since, until_exclusive),
|
||||
model_input: include_input.then_some(rp.model_input),
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -209,18 +209,10 @@ pub async fn run_fold(
|
||||
.await
|
||||
.map_err(|e| crate::Error::Runner(format!("runner task panicked: {e}")))??;
|
||||
|
||||
// Post-spend: every failure from here on salvages the paid-for output.
|
||||
// Post-spend: the response IS the artifact; only the version fence can
|
||||
// still reject it, and that path salvages the paid-for output.
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let artifact = match complete_run(&spec, chain.last(), &run_plan, &output, now) {
|
||||
Ok(a) => a,
|
||||
Err(crate::Error::Nonconforming(reason)) => {
|
||||
return Ok(RunOutcome::Refused {
|
||||
reason,
|
||||
model_output: output,
|
||||
});
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
};
|
||||
let artifact = complete_run(&spec, chain.last(), &run_plan, &output, now);
|
||||
if let Err(e) = store.insert_artifact(&artifact).await {
|
||||
// The (fold, version) primary key is the fence; a violation means a
|
||||
// concurrent writer won. Nothing forked, nothing persisted here.
|
||||
@@ -291,26 +283,49 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn preflight_prices_before_any_spend() {
|
||||
let store = store_with_fold_and_signal().await;
|
||||
let out = preflight(&store, "weekly", 0, 1_000)
|
||||
let out = preflight(&store, "weekly", 0, 1_000, false)
|
||||
.await
|
||||
.expect("preflight");
|
||||
match out {
|
||||
PreflightOutcome::Ready { shown, pending, .. } => {
|
||||
PreflightOutcome::Ready {
|
||||
shown,
|
||||
pending,
|
||||
model_input,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(shown, 1);
|
||||
// Engine semantics: pending counts every new signal in the
|
||||
// window, including the ones that would be shown.
|
||||
assert_eq!(pending, 1);
|
||||
assert!(model_input.is_none(), "input only rides when asked for");
|
||||
}
|
||||
other => panic!("expected ready, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_persists_conforming_output_and_caches_next() {
|
||||
async fn preflight_can_expose_the_exact_model_input() {
|
||||
let store = store_with_fold_and_signal().await;
|
||||
let out = preflight(&store, "weekly", 0, 1_000, true)
|
||||
.await
|
||||
.expect("preflight");
|
||||
match out {
|
||||
PreflightOutcome::Ready { model_input, .. } => {
|
||||
let input = model_input.expect("requested input");
|
||||
assert!(input.contains("digest the channel"), "task rides in input");
|
||||
assert!(input.contains("hello world"), "events ride in input");
|
||||
}
|
||||
other => panic!("expected ready, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_persists_output_verbatim_and_caches_next() {
|
||||
let store = store_with_fold_and_signal().await;
|
||||
let id = "e".repeat(64);
|
||||
let output = format!("# Working Context\nsummary\n\n# Log\n- hello [event:{id}]\n");
|
||||
let runner = Arc::new(FakeRunner(output));
|
||||
// Free-form output — any shape the task asked for persists verbatim.
|
||||
let output = format!("Just a paragraph citing [event:{id}], no headings.");
|
||||
let runner = Arc::new(FakeRunner(output.clone()));
|
||||
let guard = RunGuard::default();
|
||||
let out = run_fold(&store, runner.clone(), &guard, "weekly", 0, 1_000)
|
||||
.await
|
||||
@@ -322,6 +337,7 @@ mod tests {
|
||||
assert_eq!(artifact.version, 1);
|
||||
assert_eq!(shown, 1);
|
||||
assert_eq!(artifact.shown_ids, vec![id.clone()]);
|
||||
assert_eq!(artifact.output, output, "response persists verbatim");
|
||||
}
|
||||
other => panic!("expected folded, got {other:?}"),
|
||||
}
|
||||
@@ -333,26 +349,10 @@ mod tests {
|
||||
assert_eq!(store.artifacts("weekly").await.expect("chain").len(), 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn nonconforming_output_is_refused_and_salvaged() {
|
||||
let store = store_with_fold_and_signal().await;
|
||||
let runner = Arc::new(FakeRunner("no headings at all".into()));
|
||||
let out = run_fold(&store, runner, &RunGuard::default(), "weekly", 0, 1_000)
|
||||
.await
|
||||
.expect("run");
|
||||
match out {
|
||||
RunOutcome::Refused { model_output, .. } => {
|
||||
assert_eq!(model_output, "no headings at all");
|
||||
}
|
||||
other => panic!("expected refused, got {other:?}"),
|
||||
}
|
||||
assert!(store.artifacts("weekly").await.expect("chain").is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unknown_fold_is_not_found() {
|
||||
let store = Store::open(":memory:").await.expect("open");
|
||||
let err = preflight(&store, "nope", 0, 10)
|
||||
let err = preflight(&store, "nope", 0, 10, false)
|
||||
.await
|
||||
.expect_err("missing fold");
|
||||
assert!(matches!(err, FoldError::NotFound(_)));
|
||||
|
||||
@@ -15,7 +15,7 @@ use axum::{Json, Router};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use crate::runner::FoldRunner;
|
||||
use crate::{schema, FoldSpec, Selection};
|
||||
use crate::{FoldSpec, Selection};
|
||||
|
||||
use super::folds::{self, FoldError, RunGuard};
|
||||
use super::status::StatusRegistry;
|
||||
@@ -279,9 +279,9 @@ async fn get_fold(
|
||||
struct PutFoldBody {
|
||||
selection: Selection,
|
||||
model: String,
|
||||
/// Defaults to the built-in channel digest prompt.
|
||||
/// Defaults to the built-in running-digest task.
|
||||
instructions: Option<String>,
|
||||
/// Defaults to `channel-digest@v1` (the only schema).
|
||||
/// Provenance label recorded on artifacts; defaults to `freeform@v1`.
|
||||
schema: Option<String>,
|
||||
/// Free-form client-owned JSON, stored verbatim and never read by the
|
||||
/// engine (see [`FoldSpec::meta`]).
|
||||
@@ -298,12 +298,12 @@ async fn put_fold(
|
||||
selection: body.selection,
|
||||
schema: body
|
||||
.schema
|
||||
.unwrap_or_else(|| schema::CHANNEL_DIGEST_V1.name.to_string()),
|
||||
.unwrap_or_else(|| crate::spec::FREEFORM_SCHEMA.to_string()),
|
||||
model: body.model,
|
||||
instructions: body
|
||||
.instructions
|
||||
.filter(|i| !i.trim().is_empty())
|
||||
.unwrap_or_else(|| schema::CHANNEL_DIGEST_PROMPT.to_string()),
|
||||
.unwrap_or_else(|| crate::spec::DEFAULT_INSTRUCTIONS.to_string()),
|
||||
meta: body.meta,
|
||||
};
|
||||
spec.validate()?;
|
||||
@@ -327,15 +327,24 @@ async fn delete_fold(
|
||||
Ok(Json(json!({ "deleted": name })))
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, serde::Deserialize)]
|
||||
struct PreflightBody {
|
||||
#[serde(flatten)]
|
||||
window: WindowBody,
|
||||
/// When true, the Ready plan carries the exact string the model would
|
||||
/// receive — the "show me what's behind the curtain" seam.
|
||||
#[serde(default)]
|
||||
include_input: bool,
|
||||
}
|
||||
|
||||
async fn preflight_fold(
|
||||
State(s): State<AppState>,
|
||||
Path(name): Path<String>,
|
||||
body: Option<Json<WindowBody>>,
|
||||
body: Option<Json<PreflightBody>>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let (since, until) = body
|
||||
.map(|Json(w)| w.resolve())
|
||||
.unwrap_or_else(|| WindowBody::default().resolve());
|
||||
let out = folds::preflight(&s.store, &name, since, until).await?;
|
||||
let Json(body) = body.unwrap_or_default();
|
||||
let (since, until) = body.window.resolve();
|
||||
let out = folds::preflight(&s.store, &name, since, until, body.include_input).await?;
|
||||
Ok(Json(
|
||||
serde_json::to_value(out).unwrap_or_else(|_| json!({})),
|
||||
))
|
||||
@@ -454,7 +463,7 @@ mod tests {
|
||||
let resp = app.clone().oneshot(put).await.expect("resp");
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let saved = body_json(resp).await;
|
||||
assert_eq!(saved["saved"]["schema"], "channel-digest@v1");
|
||||
assert_eq!(saved["saved"]["schema"], "freeform@v1");
|
||||
assert!(!saved["saved"]["instructions"]
|
||||
.as_str()
|
||||
.unwrap_or("")
|
||||
|
||||
@@ -2,21 +2,15 @@
|
||||
|
||||
/// Errors surfaced by the fold engine.
|
||||
///
|
||||
/// A [`Error::Nonconforming`] means the model's output broke the artifact
|
||||
/// contract — the run is refused and nothing may be persisted. This is a
|
||||
/// deliberate design rule: never build a validator that a model can only
|
||||
/// satisfy by lying; every rule enforced here is one the model can genuinely
|
||||
/// meet.
|
||||
/// There is deliberately no "model output rejected" variant: the paid-for
|
||||
/// response persists verbatim as the artifact, and provenance is
|
||||
/// engine-computed from the plan, never parsed out of the output.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// The fold spec is invalid (bad name, empty selection, unknown schema, …).
|
||||
/// The fold spec is invalid (bad name, empty selection, blank field, …).
|
||||
#[error("invalid fold spec: {0}")]
|
||||
InvalidSpec(String),
|
||||
|
||||
/// Model output does not conform to the artifact contract; nothing persists.
|
||||
#[error("nonconforming model output: {0}")]
|
||||
Nonconforming(String),
|
||||
|
||||
/// The model runner failed (missing binary, timeout, crash, auth sentinel).
|
||||
#[error("fold runner failed: {0}")]
|
||||
Runner(String),
|
||||
|
||||
@@ -10,17 +10,16 @@
|
||||
//! - **signal** — a raw relay event, never rewritten; the source of truth.
|
||||
//! - **selection** — a saveable query over signals (channels, authors, kinds).
|
||||
//! - **fold** — `artifact' = fold(artifact, new_signals, spec)`.
|
||||
//! - **artifact** — compact sectioned output where every claim cites the exact
|
||||
//! signals it came from; persisted locally as an immutable, append-only
|
||||
//! version chain.
|
||||
//! - **artifact** — the model's response, verbatim and free-form, persisted
|
||||
//! locally as an immutable, append-only version chain whose provenance
|
||||
//! (exactly which signals went in, and when) is engine-computed.
|
||||
//! - (a **lens** renders artifacts into UI; rendering is out of scope here.)
|
||||
//!
|
||||
//! The engine modules are deliberately **pure**: they plan runs, price them,
|
||||
//! render transcripts, validate model output, and splice history — but perform
|
||||
//! no relay I/O and hold no storage. Callers (the `buzz` CLI, the [`daemon`])
|
||||
//! fetch signals and prior artifacts, hand them to [`run::plan_run`], invoke a
|
||||
//! [`runner::FoldRunner`] if the plan is ready, and finish with
|
||||
//! [`run::complete_run`].
|
||||
//! and render transcripts — but perform no relay I/O and hold no storage.
|
||||
//! Callers (the `buzz` CLI, the [`daemon`]) fetch signals and prior
|
||||
//! artifacts, hand them to [`run::plan_run`], invoke a [`runner::FoldRunner`]
|
||||
//! if the plan is ready, and finish with [`run::complete_run`].
|
||||
//!
|
||||
//! The optional `daemon` feature (default; engine consumers opt out with
|
||||
//! `default-features = false`) adds the standalone headless app: it borrows a
|
||||
@@ -36,11 +35,10 @@
|
||||
//! - Coverage records exactly the signals the model was shown — an oversized
|
||||
//! window stalls or truncates honestly; unread signals are never sealed
|
||||
//! ([`run`]).
|
||||
//! - Output that breaks the artifact contract — wrong sections, a citation of
|
||||
//! a signal the model was not shown, or no citation at all — is refused and
|
||||
//! nothing persists ([`validate`]).
|
||||
//! - Prior append-section history is retained by the engine, not the model:
|
||||
//! history cannot be rewritten regardless of model output ([`validate::splice_append_sections`]).
|
||||
//! - Provenance is engine-owned, never read from model output: `shown_ids`
|
||||
//! and the coverage window come from the plan. The output itself is
|
||||
//! free-form — `[event:<id>]` citations in it are reader-verifiable links,
|
||||
//! not a validated contract ([`artifact`]).
|
||||
|
||||
pub mod artifact;
|
||||
#[cfg(feature = "daemon")]
|
||||
@@ -49,12 +47,10 @@ pub mod error;
|
||||
pub mod estimate;
|
||||
pub mod run;
|
||||
pub mod runner;
|
||||
pub mod schema;
|
||||
pub mod selection;
|
||||
pub mod signal;
|
||||
pub mod spec;
|
||||
pub mod transcript;
|
||||
pub mod validate;
|
||||
|
||||
pub use artifact::ArtifactPayload;
|
||||
pub use error::Error;
|
||||
|
||||
@@ -7,21 +7,20 @@
|
||||
//! call, no I/O.
|
||||
//! 2. The caller shows the estimate (priced before spend), invokes a
|
||||
//! [`crate::runner::FoldRunner`] with `RunPlan::model_input`, then
|
||||
//! 3. [`complete_run`] — validate the output against the artifact contract,
|
||||
//! splice history, and produce the next [`ArtifactPayload`]. Nonconforming
|
||||
//! output returns an error and nothing persists.
|
||||
//! 3. [`complete_run`] — record the output verbatim with engine-computed
|
||||
//! provenance (exactly the shown signals and their window) as the next
|
||||
//! [`ArtifactPayload`]. The output's shape is the task's business, not the
|
||||
//! engine's.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use crate::artifact::ArtifactPayload;
|
||||
use crate::error::Error;
|
||||
use crate::estimate::{self, Estimate};
|
||||
use crate::schema;
|
||||
use crate::selection::materialize;
|
||||
use crate::signal::Signal;
|
||||
use crate::spec::FoldSpec;
|
||||
use crate::transcript::{render_transcript, ShownSignal};
|
||||
use crate::validate::{splice_append_sections, validate_output};
|
||||
|
||||
/// Hard character budget for one run's model input (prior digest + transcript).
|
||||
pub const MAX_CONTEXT_CHARS: usize = 120_000;
|
||||
@@ -37,6 +36,17 @@ pub const MAX_SHOWN_PER_RUN: usize = 250;
|
||||
/// that failure priced-before-spend instead of a wasted model call.
|
||||
pub const MAX_PRIOR_OUTPUT_BYTES: usize = 40_000;
|
||||
|
||||
/// Engine note composed into every model input between the task and the
|
||||
/// context: what the context is, and how citations work. The output shape is
|
||||
/// deliberately unconstrained — provenance is engine-owned, so citations are
|
||||
/// reader-verifiable links, not a contract.
|
||||
const ENGINE_GUIDANCE: &str = "The context below is your evidence: the prior version of this \
|
||||
document (when one exists) followed by new time-ordered events. Produce the next version of \
|
||||
the document — whatever shape the task asks for. Where useful, cite the events behind a claim \
|
||||
as [event:<id>], one id per bracket, copied in full from the SOURCE EVENT IDS list; citations \
|
||||
render as clickable links to the source, so a wrong or invented id shows readers a dead link. \
|
||||
Do not invent facts. Output only the document.";
|
||||
|
||||
/// The outcome of planning a run.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Plan {
|
||||
@@ -110,7 +120,7 @@ pub fn plan_run(
|
||||
}
|
||||
let parent = match prior {
|
||||
Some(p) => format!(
|
||||
"--- PRIOR DIGEST ({} v{}) ---\n{}",
|
||||
"--- PRIOR VERSION ({} v{}) ---\n{}",
|
||||
spec.name, p.version, p.output
|
||||
),
|
||||
None => String::new(),
|
||||
@@ -136,14 +146,11 @@ pub fn plan_run(
|
||||
transcript.push_str("\n\n--- SOURCE EVENT IDS ---\n");
|
||||
transcript.push_str(&ids.join("\n"));
|
||||
}
|
||||
// Instructions are the task; the schema contract is non-negotiable and
|
||||
// travels with every run — otherwise custom instructions silently drop
|
||||
// the structural rules that complete_run() will refuse on.
|
||||
// Instructions are the task and the output is free-form; the engine only
|
||||
// explains what the context is and how citations render.
|
||||
let model_input = format!(
|
||||
"--- TASK ---\n{}\n\n{}\n\n--- CONTEXT (time-ordered events) ---\n{}\n",
|
||||
spec.instructions,
|
||||
schema::CHANNEL_DIGEST_V1.contract_prompt(),
|
||||
transcript
|
||||
spec.instructions, ENGINE_GUIDANCE, transcript
|
||||
);
|
||||
let est = estimate::estimate(&spec.model, model_input.chars().count());
|
||||
Ok(Plan::Ready(RunPlan {
|
||||
@@ -155,25 +162,19 @@ pub fn plan_run(
|
||||
}))
|
||||
}
|
||||
|
||||
/// Validate model `output` for `plan` and produce the next artifact version.
|
||||
/// Record model `output` for `plan` verbatim as the next artifact version.
|
||||
///
|
||||
/// On a structural violation (wrong H1 sections) this returns
|
||||
/// [`Error::Nonconforming`] and the caller must persist nothing. Coverage is
|
||||
/// computed from exactly the signals the plan showed — provenance is
|
||||
/// engine-owned, not read from the output — and append-section history from
|
||||
/// `prior` is spliced back mechanically, so it survives regardless of what
|
||||
/// the model emitted.
|
||||
/// The output's shape is the task's business — nothing about the text is
|
||||
/// judged. Provenance is engine-owned: coverage is computed from exactly the
|
||||
/// signals the plan showed, never from what the output claims.
|
||||
pub fn complete_run(
|
||||
spec: &FoldSpec,
|
||||
prior: Option<&ArtifactPayload>,
|
||||
plan: &RunPlan,
|
||||
output: &str,
|
||||
created_at: i64,
|
||||
) -> Result<ArtifactPayload, Error> {
|
||||
let sch = &schema::CHANNEL_DIGEST_V1;
|
||||
) -> ArtifactPayload {
|
||||
let shown_ids: Vec<String> = plan.shown.iter().map(|s| s.id.clone()).collect();
|
||||
validate_output(sch, output)?;
|
||||
let spliced = splice_append_sections(sch, prior.map(|p| p.output.as_str()), output);
|
||||
// Taint travels with the chain: once a channel's events fold in, every
|
||||
// later version keeps carrying that channel, even after a selection edit.
|
||||
let channels: BTreeSet<String> = prior
|
||||
@@ -182,10 +183,10 @@ pub fn complete_run(
|
||||
.union(&spec.selection.channels.iter().cloned().collect())
|
||||
.cloned()
|
||||
.collect();
|
||||
Ok(ArtifactPayload {
|
||||
ArtifactPayload {
|
||||
fold: spec.name.clone(),
|
||||
version: prior.map_or(1, |p| p.version + 1),
|
||||
output: spliced,
|
||||
output: output.to_string(),
|
||||
coverage_since: plan.shown.iter().map(|s| s.created_at).min(),
|
||||
coverage_until: plan.shown.iter().map(|s| s.created_at).max().map(|t| t + 1),
|
||||
selection: spec.selection.clone(),
|
||||
@@ -196,7 +197,7 @@ pub fn complete_run(
|
||||
prompt_sha256: spec.prompt_sha256(),
|
||||
truncated: plan.truncated,
|
||||
created_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -295,15 +296,21 @@ mod tests {
|
||||
assert!(!run.model_input.contains("--- SOURCE EVENT IDS ---"));
|
||||
assert!(run
|
||||
.model_input
|
||||
.contains("--- PRIOR DIGEST (team-digest v1) ---"));
|
||||
// No shown signals → citation contract not demanded, coverage stays empty.
|
||||
let v2 = complete_run(&spec, Some(&prior), &run, &digest(""), 1_700_000_200).expect("v2");
|
||||
.contains("--- PRIOR VERSION (team-digest v1) ---"));
|
||||
// No shown signals → nothing new to cover, coverage stays empty.
|
||||
let v2 = complete_run(
|
||||
&spec,
|
||||
Some(&prior),
|
||||
&run,
|
||||
"rewritten under new task",
|
||||
1_700_000_200,
|
||||
);
|
||||
assert_eq!(v2.version, 2);
|
||||
assert!(v2.shown_ids.is_empty());
|
||||
assert_eq!(v2.coverage_since, None);
|
||||
assert_eq!(v2.coverage_until, None);
|
||||
// The engine splice still carried the prior Log forward.
|
||||
assert!(v2.output.contains("- prior entry"));
|
||||
// The response persists verbatim — retention is the model's job now.
|
||||
assert_eq!(v2.output, "rewritten under new task");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -371,7 +378,7 @@ mod tests {
|
||||
panic!("expected Ready");
|
||||
};
|
||||
let out = digest(&format!("- new entry [event:{b}]"));
|
||||
let v2 = complete_run(&spec, Some(&prior), &run, &out, 1_700_000_500).expect("v2");
|
||||
let v2 = complete_run(&spec, Some(&prior), &run, &out, 1_700_000_500);
|
||||
assert_eq!(v2.channels, vec!["ch1".to_string(), "ch2".to_string()]);
|
||||
assert_eq!(v2.selection, spec.selection);
|
||||
}
|
||||
@@ -403,9 +410,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_instructions_cannot_drop_the_output_contract() {
|
||||
// A task-only prompt ("what is this project about?") must still carry
|
||||
// the schema's structural rules, or validation is a guaranteed refusal.
|
||||
fn every_run_carries_the_task_and_the_engine_guidance() {
|
||||
// The engine explains what the context is and how citations render;
|
||||
// the task is the caller's, verbatim.
|
||||
let mut spec = spec();
|
||||
spec.instructions = "what is this project trying to accomplish".to_string();
|
||||
let plan = plan_run(
|
||||
@@ -422,9 +429,8 @@ mod tests {
|
||||
assert!(run
|
||||
.model_input
|
||||
.contains("what is this project trying to accomplish"));
|
||||
assert!(run.model_input.contains("OUTPUT CONTRACT"));
|
||||
assert!(run.model_input.contains("Working Context, Log"));
|
||||
assert!(run.model_input.contains("Log is append-only"));
|
||||
assert!(run.model_input.contains("whatever shape the task asks for"));
|
||||
assert!(run.model_input.contains("SOURCE EVENT IDS"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -445,7 +451,7 @@ mod tests {
|
||||
// Coverage seals only what was shown; the rest stays pending.
|
||||
let cited = run.shown[0].id.clone();
|
||||
let out = digest(&format!("- chunk one [event:{cited}]"));
|
||||
let v1 = complete_run(&spec, None, &run, &out, 1_700_000_300).expect("v1");
|
||||
let v1 = complete_run(&spec, None, &run, &out, 1_700_000_300);
|
||||
assert_eq!(v1.shown_ids.len(), run.shown.len());
|
||||
assert!(v1.truncated);
|
||||
}
|
||||
@@ -466,7 +472,7 @@ mod tests {
|
||||
panic!("expected Ready");
|
||||
};
|
||||
let out = digest(&format!("- said hello [event:{b}]"));
|
||||
let v1 = complete_run(&spec, None, &run, &out, 1_700_000_300).expect("v1");
|
||||
let v1 = complete_run(&spec, None, &run, &out, 1_700_000_300);
|
||||
assert_eq!(v1.version, 1);
|
||||
assert_eq!(v1.shown_ids, vec![b]);
|
||||
assert_eq!(v1.coverage_since, Some(200));
|
||||
@@ -475,7 +481,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn complete_run_splices_prior_history_and_increments_version() {
|
||||
fn output_is_verbatim_and_the_prior_rides_in_the_input() {
|
||||
let spec = spec();
|
||||
let a = hex_id('a');
|
||||
let b = hex_id('b');
|
||||
@@ -492,38 +498,16 @@ mod tests {
|
||||
let Plan::Ready(run) = plan else {
|
||||
panic!("expected Ready");
|
||||
};
|
||||
// Model returns ONLY the new entry; engine must carry the prior Log.
|
||||
let out = digest(&format!("- new entry [event:{b}]"));
|
||||
let v2 = complete_run(&spec, Some(&prior), &run, &out, 1_700_000_400).expect("v2");
|
||||
// The prior version is the model's context, labeled as such…
|
||||
assert!(run
|
||||
.model_input
|
||||
.contains("--- PRIOR VERSION (team-digest v1) ---"));
|
||||
// …and the model's response IS the artifact: any shape, verbatim, no
|
||||
// splice. Provenance (version, coverage) is engine bookkeeping.
|
||||
let out = "A totally free-form rewrite that dropped the prior entry.";
|
||||
let v2 = complete_run(&spec, Some(&prior), &run, out, 1_700_000_400);
|
||||
assert_eq!(v2.version, 2);
|
||||
assert!(v2.output.contains("- prior entry"));
|
||||
assert!(v2.output.contains("- new entry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_sections_are_refused_but_citations_never_are() {
|
||||
let spec = spec();
|
||||
let b = hex_id('b');
|
||||
let plan = plan_run(
|
||||
&spec,
|
||||
None,
|
||||
&BTreeSet::new(),
|
||||
vec![sig(&b, 200, "hello")],
|
||||
&BTreeMap::new(),
|
||||
)
|
||||
.expect("plan");
|
||||
let Plan::Ready(run) = plan else {
|
||||
panic!("expected Ready");
|
||||
};
|
||||
// Wrong sections: the structure is the contract, still refused.
|
||||
assert!(matches!(
|
||||
complete_run(&spec, None, &run, "# Nope\n\nbody\n", 0),
|
||||
Err(Error::Nonconforming(_))
|
||||
));
|
||||
// An unshown citation persists: coverage still seals exactly what the
|
||||
// plan showed, independent of what the text cites.
|
||||
let unshown = digest(&format!("- quoted id [event:{}]", hex_id('c')));
|
||||
let v1 = complete_run(&spec, None, &run, &unshown, 0).expect("v1");
|
||||
assert_eq!(v1.shown_ids, vec![b]);
|
||||
assert_eq!(v2.output, out);
|
||||
assert_eq!(v2.shown_ids, vec![b]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
//! Artifact schemas: the structural contract a fold's output must satisfy.
|
||||
|
||||
/// A schema names the exact H1 sections an artifact must have, and which of
|
||||
/// them are *append* sections — sections whose history the engine retains
|
||||
/// mechanically ([`crate::validate::splice_append_sections`]) so the model
|
||||
/// only ever contributes new entries.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ArtifactSchema {
|
||||
/// Versioned schema name, e.g. `channel-digest@v1`.
|
||||
pub name: &'static str,
|
||||
/// Required H1 sections, in order.
|
||||
pub sections: &'static [&'static str],
|
||||
/// Sections that accumulate: prior content is preserved by construction
|
||||
/// and new entries must carry signal citations.
|
||||
pub append_sections: &'static [&'static str],
|
||||
}
|
||||
|
||||
/// A bounded digest of a selection: a rewritten standing summary plus an
|
||||
/// append-only, citation-carrying log. The only schema; the `schema` string
|
||||
/// on specs and artifacts stays as the versioning seam for a future second
|
||||
/// one.
|
||||
pub const CHANNEL_DIGEST_V1: ArtifactSchema = ArtifactSchema {
|
||||
name: "channel-digest@v1",
|
||||
sections: &["Working Context", "Log"],
|
||||
append_sections: &["Log"],
|
||||
};
|
||||
|
||||
impl ArtifactSchema {
|
||||
/// The output contract, rendered as prompt text. [`crate::plan_run`]
|
||||
/// composes this into every model input *alongside* the spec's
|
||||
/// instructions, so custom instructions focus the task but can never drop
|
||||
/// the structural rules that [`crate::validate::validate_output`] will
|
||||
/// enforce — otherwise a task-only prompt is a guaranteed refusal.
|
||||
pub fn contract_prompt(&self) -> String {
|
||||
let mut s = format!(
|
||||
"OUTPUT CONTRACT (the section structure is mechanically enforced; output with \
|
||||
different H1 sections is refused and nothing persists):\nWhatever the task above \
|
||||
asks for, deliver it as markdown with exactly these H1 sections, in this order: \
|
||||
{}.\n",
|
||||
self.sections.join(", ")
|
||||
);
|
||||
for a in self.append_sections {
|
||||
s.push_str(&format!(
|
||||
"{a} is append-only: output ONLY new dated entries for this run's evidence \
|
||||
— the engine keeps the standing {a} and appends your new entries to it; \
|
||||
never repeat prior entries.\n"
|
||||
));
|
||||
}
|
||||
s.push_str(
|
||||
"Cite the source events you actually use as [event:<id>], one id per bracket, \
|
||||
copied in full from the SOURCE EVENT IDS list — citations render as clickable \
|
||||
links to the source, so a wrong or invented id shows readers a dead link. Do \
|
||||
not invent facts. Output only the document markdown.",
|
||||
);
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
/// Default fold *task* instructions for [`CHANNEL_DIGEST_V1`]. The structural
|
||||
/// rules live in [`ArtifactSchema::contract_prompt`], which every run gets
|
||||
/// regardless of what the instructions say.
|
||||
pub const CHANNEL_DIGEST_PROMPT: &str = "Maintain a bounded digest of the anchored selection. \
|
||||
Working Context is a concise standing summary rewritten in light of all evidence; the Log \
|
||||
records dated evidence entries.";
|
||||
@@ -9,12 +9,21 @@ use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::schema;
|
||||
use crate::selection::Selection;
|
||||
|
||||
/// Maximum fold-name length (also the `d` tag value).
|
||||
pub const MAX_NAME_LEN: usize = 64;
|
||||
|
||||
/// Default schema label for new folds. The output is free-form — the label is
|
||||
/// provenance on artifacts (which prompt regime produced them), not a
|
||||
/// validated contract; chains created under `channel-digest@v1` keep their
|
||||
/// historical label.
|
||||
pub const FREEFORM_SCHEMA: &str = "freeform@v1";
|
||||
|
||||
/// Default fold *task* instructions when the caller supplies none.
|
||||
pub const DEFAULT_INSTRUCTIONS: &str = "Maintain a running digest of the selection: rewrite the \
|
||||
prior version in light of the new events. Keep it concise, concrete, and factual.";
|
||||
|
||||
/// One fold definition.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct FoldSpec {
|
||||
@@ -22,7 +31,8 @@ pub struct FoldSpec {
|
||||
pub name: String,
|
||||
/// What to read.
|
||||
pub selection: Selection,
|
||||
/// Schema name; the only one is [`crate::schema::CHANNEL_DIGEST_V1`].
|
||||
/// Schema label recorded on artifacts as provenance ([`FREEFORM_SCHEMA`]
|
||||
/// by default). Output is free-form; the label is not a validated shape.
|
||||
pub schema: String,
|
||||
/// Model alias passed to the runner and priced by [`crate::estimate`].
|
||||
pub model: String,
|
||||
@@ -55,12 +65,10 @@ impl FoldSpec {
|
||||
self.name
|
||||
)));
|
||||
}
|
||||
if self.schema != schema::CHANNEL_DIGEST_V1.name {
|
||||
return Err(Error::InvalidSpec(format!(
|
||||
"unknown schema {:?}; the only schema is {:?}",
|
||||
self.schema,
|
||||
schema::CHANNEL_DIGEST_V1.name
|
||||
)));
|
||||
if self.schema.trim().is_empty() {
|
||||
return Err(Error::InvalidSpec(
|
||||
"schema label must be non-empty (it is recorded on artifacts as provenance)".into(),
|
||||
));
|
||||
}
|
||||
if self.model.trim().is_empty() {
|
||||
return Err(Error::InvalidSpec(
|
||||
@@ -116,9 +124,13 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_schema_and_empty_fields_are_rejected() {
|
||||
fn empty_fields_are_rejected_but_any_schema_label_passes() {
|
||||
// Schema is a provenance label, not a validated shape.
|
||||
let mut s = valid();
|
||||
s.schema = "nope@v1".to_string();
|
||||
s.schema = "my-custom-regime@v3".to_string();
|
||||
assert!(s.validate().is_ok());
|
||||
let mut s = valid();
|
||||
s.schema = " ".to_string();
|
||||
assert!(s.validate().is_err());
|
||||
let mut s = valid();
|
||||
s.model = " ".to_string();
|
||||
|
||||
@@ -1,227 +0,0 @@
|
||||
//! Output-contract validation and the engine splice.
|
||||
//!
|
||||
//! Only the section STRUCTURE is validated — it is what the splice and every
|
||||
//! reader depend on. Provenance is engine-owned and deterministic (the run's
|
||||
//! `shown_ids` and coverage window are computed from the plan, never from
|
||||
//! model output), so `[event:…]` citations in the text are best-effort links
|
||||
//! for the reader, not a validated contract: a sloppy citation renders as a
|
||||
//! dead chip instead of refusing a paid run.
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::schema::ArtifactSchema;
|
||||
|
||||
/// True for a line the heading grammar recognizes as an H1: `# ` followed by
|
||||
/// at least one non-`#` character.
|
||||
fn is_h1(line: &str) -> bool {
|
||||
match line.strip_prefix("# ") {
|
||||
Some(rest) => rest.chars().next().is_some_and(|c| c != '#'),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Byte offsets of the section body for H1 `name`: (start just after the
|
||||
/// heading's newline, end at the next H1 line or end of input). `None` when
|
||||
/// the heading is absent (a heading with no trailing newline has no body).
|
||||
fn section_span(output: &str, name: &str) -> Option<(usize, usize)> {
|
||||
let mut pos = 0;
|
||||
let mut found: Option<usize> = None;
|
||||
for line in output.split_inclusive('\n') {
|
||||
let bare = line.strip_suffix('\n');
|
||||
let text = bare.unwrap_or(line);
|
||||
match found {
|
||||
None => {
|
||||
// Heading line: `# {name}` plus optional trailing whitespace,
|
||||
// and it must be newline-terminated to have a body.
|
||||
let matches = text
|
||||
.strip_prefix("# ")
|
||||
.is_some_and(|rest| rest.trim_end() == name && rest.trim_end() == rest.trim());
|
||||
if matches && bare.is_some() {
|
||||
found = Some(pos + line.len());
|
||||
}
|
||||
}
|
||||
Some(start) => {
|
||||
if is_h1(text) {
|
||||
return Some((start, pos));
|
||||
}
|
||||
}
|
||||
}
|
||||
pos += line.len();
|
||||
}
|
||||
found.map(|start| (start, output.len()))
|
||||
}
|
||||
|
||||
/// The trimmed body of H1 section `name`, or `""` when absent.
|
||||
pub fn section(output: &str, name: &str) -> String {
|
||||
match section_span(output, name) {
|
||||
Some((start, end)) => output[start..end].trim().to_string(),
|
||||
None => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the body of H1 section `name` with `content`, leaving the rest of
|
||||
/// the document byte-identical. A missing heading leaves the document
|
||||
/// unchanged.
|
||||
pub fn replace_section(output: &str, name: &str, content: &str) -> String {
|
||||
match section_span(output, name) {
|
||||
Some((start, end)) => {
|
||||
format!("{}\n{}\n\n{}", &output[..start], content, &output[end..])
|
||||
}
|
||||
None => output.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// All H1 headings in document order (text after `# `, verbatim to line end).
|
||||
pub fn headings(output: &str) -> Vec<String> {
|
||||
output
|
||||
.lines()
|
||||
.filter(|line| is_h1(line))
|
||||
.map(|line| line[2..].to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Validate model output against the artifact contract: the output must have
|
||||
/// exactly the schema's H1 sections, in order. That is the whole contract —
|
||||
/// the splice and the reader depend on the structure; nothing else about the
|
||||
/// text is judged. Failure is [`Error::Nonconforming`]: the caller persists
|
||||
/// nothing.
|
||||
pub fn validate_output(schema: &ArtifactSchema, output: &str) -> Result<(), Error> {
|
||||
let found = headings(output);
|
||||
if found != schema.sections {
|
||||
return Err(Error::Nonconforming(format!(
|
||||
"output does not conform to {}: expected H1 sections {:?}, got {:?}",
|
||||
schema.name, schema.sections, found
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Rebuild append sections so prior content is retained by construction.
|
||||
///
|
||||
/// The model contributes only this run's NEW entries; the engine prepends the
|
||||
/// prior section verbatim. A model that repeats the prior section anyway is
|
||||
/// deduplicated (exact-substring strip). History cannot be rewritten or
|
||||
/// dropped no matter what the model outputs — and the model never has to
|
||||
/// re-emit an ever-growing log, which kept run latency flat along the chain.
|
||||
pub fn splice_append_sections(
|
||||
schema: &ArtifactSchema,
|
||||
previous_output: Option<&str>,
|
||||
output: &str,
|
||||
) -> String {
|
||||
let Some(previous) = previous_output.filter(|p| !p.is_empty()) else {
|
||||
return output.to_string();
|
||||
};
|
||||
let mut doc = output.to_string();
|
||||
for name in schema.append_sections {
|
||||
let prior = section(previous, name);
|
||||
if prior.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let model_part = section(&doc, name);
|
||||
let new_part = if model_part.contains(&prior) {
|
||||
model_part.replacen(&prior, "", 1)
|
||||
} else {
|
||||
model_part
|
||||
};
|
||||
let new_part = new_part.trim();
|
||||
let combined = if new_part.is_empty() {
|
||||
prior
|
||||
} else {
|
||||
format!("{prior}\n{new_part}")
|
||||
};
|
||||
doc = replace_section(&doc, name, &combined);
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::schema::CHANNEL_DIGEST_V1;
|
||||
|
||||
fn id(c: char) -> String {
|
||||
std::iter::repeat_n(c, 64).collect()
|
||||
}
|
||||
|
||||
fn digest(log_body: &str) -> String {
|
||||
format!("# Working Context\n\nSummary.\n\n# Log\n\n{log_body}\n")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_and_replace_roundtrip() {
|
||||
let doc = digest("- entry one");
|
||||
assert_eq!(section(&doc, "Log"), "- entry one");
|
||||
assert_eq!(section(&doc, "Working Context"), "Summary.");
|
||||
assert_eq!(section(&doc, "Missing"), "");
|
||||
let replaced = replace_section(&doc, "Log", "- swapped");
|
||||
assert_eq!(section(&replaced, "Log"), "- swapped");
|
||||
assert_eq!(section(&replaced, "Working Context"), "Summary.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headings_ignore_h2_and_capture_in_order() {
|
||||
let doc = "# One\n\n## nested\n\n# Two\nbody\n";
|
||||
assert_eq!(headings(doc), vec!["One", "Two"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heading_mismatch_is_refused() {
|
||||
let out = "# Wrong\n\nx\n\n# Log\n\n- e\n";
|
||||
let err = validate_output(&CHANNEL_DIGEST_V1, out);
|
||||
assert!(matches!(err, Err(Error::Nonconforming(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conforming_sections_pass_regardless_of_citations() {
|
||||
// Provenance is engine-owned (shown_ids + coverage window on the
|
||||
// artifact); citations in the text are best-effort links, never a
|
||||
// refusal. Unshown, missing, and malformed citations all pass —
|
||||
// an unresolvable chip in the reader beats losing a paid run.
|
||||
assert!(validate_output(&CHANNEL_DIGEST_V1, &digest("")).is_ok());
|
||||
assert!(validate_output(&CHANNEL_DIGEST_V1, &digest("- no citation here")).is_ok());
|
||||
let unshown = digest(&format!("- quoted from message text [event:{}]", id('c')));
|
||||
assert!(validate_output(&CHANNEL_DIGEST_V1, &unshown).is_ok());
|
||||
let malformed = digest("- sloppy [event:abc123]");
|
||||
assert!(validate_output(&CHANNEL_DIGEST_V1, &malformed).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splice_retains_prior_history_when_model_omits_it() {
|
||||
let prior = digest(&format!("- old entry [event:{}]", id('a')));
|
||||
let fresh = digest(&format!("- new entry [event:{}]", id('b')));
|
||||
let doc = splice_append_sections(&CHANNEL_DIGEST_V1, Some(&prior), &fresh);
|
||||
let log = section(&doc, "Log");
|
||||
assert_eq!(
|
||||
log,
|
||||
format!(
|
||||
"- old entry [event:{}]\n- new entry [event:{}]",
|
||||
id('a'),
|
||||
id('b')
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splice_dedupes_a_repeated_prior_section() {
|
||||
let prior = digest(&format!("- old entry [event:{}]", id('a')));
|
||||
let repeat_plus_new = digest(&format!(
|
||||
"- old entry [event:{}]\n- new entry [event:{}]",
|
||||
id('a'),
|
||||
id('b')
|
||||
));
|
||||
let doc = splice_append_sections(&CHANNEL_DIGEST_V1, Some(&prior), &repeat_plus_new);
|
||||
let log = section(&doc, "Log");
|
||||
assert_eq!(log.matches("- old entry").count(), 1);
|
||||
assert!(log.contains("- new entry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splice_with_no_new_entries_keeps_prior_exactly() {
|
||||
let prior = digest(&format!("- old entry [event:{}]", id('a')));
|
||||
let empty_log = digest("");
|
||||
let doc = splice_append_sections(&CHANNEL_DIGEST_V1, Some(&prior), &empty_log);
|
||||
assert_eq!(
|
||||
section(&doc, "Log"),
|
||||
format!("- old entry [event:{}]", id('a'))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -105,6 +105,9 @@ export type Preflight =
|
||||
truncated: boolean;
|
||||
estimate: Estimate;
|
||||
window: [number, number];
|
||||
/** The exact string the model would receive; present only when the
|
||||
* preflight asked for it (`include_input`). */
|
||||
model_input?: string;
|
||||
};
|
||||
|
||||
export type Artifact = {
|
||||
@@ -138,7 +141,6 @@ export type ArtifactSummary = {
|
||||
export type RunOutcome =
|
||||
| { status: "cached" }
|
||||
| { status: "stalled"; reason: string; pending: number }
|
||||
| { status: "refused"; reason: string; model_output: string }
|
||||
| { status: "unpublished"; reason: string; model_output: string }
|
||||
| {
|
||||
status: "folded";
|
||||
@@ -226,8 +228,11 @@ export const api = {
|
||||
) => request<{ saved: FoldSpec }>(`/folds/${name}`, { method: "PUT", ...json(body) }),
|
||||
deleteFold: (name: string) =>
|
||||
request<{ deleted: string }>(`/folds/${name}`, { method: "DELETE" }),
|
||||
preflight: (name: string, window: TimeWindow) =>
|
||||
request<Preflight>(`/folds/${name}/preflight`, post(window)),
|
||||
preflight: (name: string, window: TimeWindow, includeInput = false) =>
|
||||
request<Preflight>(
|
||||
`/folds/${name}/preflight`,
|
||||
post({ ...window, include_input: includeInput }),
|
||||
),
|
||||
run: (name: string, window: TimeWindow) =>
|
||||
request<RunOutcome>(`/folds/${name}/run`, post(window)),
|
||||
artifacts: (name: string) =>
|
||||
|
||||
@@ -10,8 +10,8 @@ type Props = {
|
||||
onVersion: (v: number) => void;
|
||||
};
|
||||
|
||||
/** Renders one artifact version: Working Context + Log with citation chips,
|
||||
* a version scrubber, and a pinned-evidence rail. */
|
||||
/** Renders one artifact version: the model's free-form response with citation
|
||||
* chips, a version scrubber, and a pinned-evidence rail. */
|
||||
export function ArtifactReader({ fold, version, onVersion }: Props) {
|
||||
const [pinned, setPinned] = useState<string[]>([]);
|
||||
|
||||
@@ -50,12 +50,21 @@ export function ArtifactReader({ fold, version, onVersion }: Props) {
|
||||
</div>
|
||||
{a && (
|
||||
<div className="muted" style={{ fontSize: 12, marginBottom: 10 }}>
|
||||
{fmtTime(a.created_at)} · model {a.model} · {a.shown_ids.length} shown
|
||||
{a.truncated ? " · chunked window" : ""}
|
||||
{fmtTime(a.created_at)} · model {a.model} · folded{" "}
|
||||
{a.shown_ids.length} new event(s)
|
||||
{a.truncated ? " · chunked (more remained pending)" : ""}
|
||||
{a.coverage_since !== null && a.coverage_until !== null && (
|
||||
<>
|
||||
{" "}
|
||||
· covers {fmtTime(a.coverage_since)} → {fmtTime(a.coverage_until)}
|
||||
· this run's new events span {fmtTime(a.coverage_since)} →{" "}
|
||||
{fmtTime(a.coverage_until)}
|
||||
</>
|
||||
)}
|
||||
{a.version > 1 && (
|
||||
<>
|
||||
{" "}
|
||||
· built on v{a.version - 1} (earlier versions' events aren't
|
||||
re-read — they ride along in the prior version)
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -84,7 +84,7 @@ function CreateFold({ selection }: { selection: Selection }) {
|
||||
<label>Instructions</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
placeholder="task focus, e.g. 'track decisions and blockers' — the Working Context / Log output contract always applies"
|
||||
placeholder="the task — instructions + context go to the model; its response IS the artifact, any shape you ask for"
|
||||
value={instructions}
|
||||
onChange={(e) => setInstructions(e.target.value)}
|
||||
/>
|
||||
@@ -120,7 +120,9 @@ function FoldCard({
|
||||
const [outcome, setOutcome] = useState<RunOutcome | null>(null);
|
||||
|
||||
const preflight = useMutation({
|
||||
mutationFn: () => api.preflight(fold.name, {}),
|
||||
// Always ask for the exact model input: the plan box offers it behind a
|
||||
// disclosure so a run is never a mystery.
|
||||
mutationFn: () => api.preflight(fold.name, {}, true),
|
||||
onSuccess: (p) => {
|
||||
setPlan(p);
|
||||
setOutcome(null);
|
||||
@@ -163,6 +165,9 @@ function FoldCard({
|
||||
? `v${fold.latest_version} · updated ${fmtTime(fold.updated_at)}`
|
||||
: "never run"}
|
||||
</div>
|
||||
<div className="desc" style={{ whiteSpace: "pre-wrap" }}>
|
||||
<span className="faint">task:</span> {fold.spec.instructions}
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button
|
||||
onClick={() => preflight.mutate()}
|
||||
@@ -210,14 +215,28 @@ function PlanBox({ plan }: { plan: Preflight }) {
|
||||
const fits = est.window_fit.fits;
|
||||
return (
|
||||
<div className="plan-box ok">
|
||||
Ready: {plan.shown} shown / {plan.pending} pending
|
||||
{plan.truncated ? " (chunked)" : ""} · ~
|
||||
Ready: {plan.shown} new event(s) to fold / {plan.pending} not yet covered
|
||||
{plan.truncated ? " (chunked — the rest stays pending)" : ""} · ~
|
||||
{fmtTokens(est.est_input_tokens)} input tokens
|
||||
{fits === false ? " · OVER the model window" : ""}
|
||||
<br />
|
||||
<span className="faint">
|
||||
Run is pinned to this priced window; press run to spend.
|
||||
Each run folds only events no earlier version covered; the prior
|
||||
version rides along as context. Run is pinned to this priced window.
|
||||
</span>
|
||||
{plan.model_input !== undefined && (
|
||||
<details style={{ marginTop: 6 }}>
|
||||
<summary className="faint" style={{ cursor: "pointer" }}>
|
||||
show the exact model input ({plan.model_input.length.toLocaleString()} chars)
|
||||
</summary>
|
||||
<pre
|
||||
className="mono"
|
||||
style={{ whiteSpace: "pre-wrap", maxHeight: 320, overflow: "auto" }}
|
||||
>
|
||||
{plan.model_input}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -232,16 +251,6 @@ function OutcomeBox({ outcome }: { outcome: RunOutcome }) {
|
||||
Stalled: {outcome.reason} ({outcome.pending} pending)
|
||||
</div>
|
||||
);
|
||||
case "refused":
|
||||
return (
|
||||
<div className="plan-box err">
|
||||
Model output refused ({outcome.reason}). Nothing persisted; raw
|
||||
output:
|
||||
<pre className="mono" style={{ whiteSpace: "pre-wrap" }}>
|
||||
{outcome.model_output.slice(0, 2000)}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
case "unpublished":
|
||||
return (
|
||||
<div className="plan-box err">
|
||||
|
||||
Reference in New Issue
Block a user