summarize: first real forge call — generate per-chapter summaries
skald summarize --story <uuid> walks every chapter without an existing summary, calls Forge::summarize() (clawdforge → opus → ~250 words of plot/character/setting/threads), and inserts the result into chapter_summaries. Side effects: - generation_runs row per chapter (kind='summary', status flow running → succeeded|failed). Errors update the row + bail; happy path closes it with ended_at + tokens. - ON CONFLICT (chapter_id) means re-running with --force replaces the previous summary cleanly. CLI: skald summarize --story <uuid> # only-missing skald summarize --story <uuid> --force # re-summarize all Reads from env (loaded by skald.env in the container): CLAWDFORGE_URL — base URL of clawdforge HTTP service CLAWDFORGE_TOKEN — app-level bearer (per-app, not the admin token) SKALD_MODEL — defaults to 'opus' This is the first subcommand that actually exercises the forge. Unlocks ContinuationContext::assemble's coverage metric (was stuck at 24%% on Coast-Down because the 5 placeholder summaries don't actually carry the prose). After running summarize against Coast-Down: coverage should jump to ~100%% and the context blob for any sequel becomes fully canon-faithful without dragging the full ~21k words of earlier-chapter prose along. Forge prompt template for summarize ships REAL (not stubbed) — it's the simplest pass and has a well-defined shape. The gen/cleanup/ audit prompts remain stubs pending the deeper prose-craft session.
This commit is contained in:
parent
b32938ef43
commit
39e991240a
3 changed files with 247 additions and 0 deletions
|
|
@ -66,6 +66,8 @@ pub enum PassKind {
|
|||
Cleanup,
|
||||
/// Canon audit across parent + sequel. Outputs findings JSON.
|
||||
Audit,
|
||||
/// Chapter summary for cheap context loading on long series.
|
||||
Summary,
|
||||
}
|
||||
|
||||
impl PassKind {
|
||||
|
|
@ -74,6 +76,7 @@ impl PassKind {
|
|||
Self::Gen => "gen",
|
||||
Self::Cleanup => "cleanup",
|
||||
Self::Audit => "audit",
|
||||
Self::Summary => "summary",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -143,8 +146,46 @@ impl Forge {
|
|||
let duration_ms = r.duration_ms;
|
||||
Ok(PassOutput { kind: PassKind::Audit, result: r, duration_ms })
|
||||
}
|
||||
|
||||
/// Summarize one chapter to ~250 words. The summary feeds into
|
||||
/// the continuation context for older chapters so the token
|
||||
/// budget stays sane on long series (book 12 doesn't carry book 1
|
||||
/// in full prose; carries summaries of books 1-10 + full prose of
|
||||
/// books 11-12).
|
||||
///
|
||||
/// Unlike gen/cleanup/audit, summarize has a real prompt template
|
||||
/// shipped here — summarization is a simple, well-defined task
|
||||
/// and doesn't need the prose-craft TODO treatment.
|
||||
pub async fn summarize(&self, chapter_body_md: &str, chapter_label: &str) -> anyhow::Result<PassOutput> {
|
||||
let prompt = format!(
|
||||
"Summarize the following chapter in ~250 words for use as future \
|
||||
sequel context. Capture: (1) plot beats in order, (2) character \
|
||||
developments and emotional shifts, (3) setting changes, (4) any \
|
||||
explicit or implied unresolved threads, (5) the chapter's \
|
||||
closing position for each named character.\n\nReturn prose only \
|
||||
— no headings, no bullet lists, no commentary about the task. \
|
||||
Write as if you're handing this to another author who needs to \
|
||||
write the next chapter without re-reading this one.\n\n\
|
||||
## {chapter_label}\n\n{chapter_body_md}"
|
||||
);
|
||||
let body = RunRequest {
|
||||
prompt,
|
||||
model: Some(self.model.clone()),
|
||||
system: Some(SYSTEM_SUMMARIZE.to_string()),
|
||||
timeout_secs: Some(300),
|
||||
..Default::default()
|
||||
};
|
||||
let r = self.client.run(body).await?;
|
||||
let duration_ms = r.duration_ms;
|
||||
Ok(PassOutput { kind: PassKind::Summary, result: r, duration_ms })
|
||||
}
|
||||
}
|
||||
|
||||
const SYSTEM_SUMMARIZE: &str = "You are a continuity assistant for a long-form \
|
||||
fiction author. You write chapter summaries that future authors of sequels \
|
||||
will read to understand what happened. Be specific. Names, dates, locations. \
|
||||
Don't editorialize — just compress the events.";
|
||||
|
||||
fn build_request(model: &str, kind: PassKind, primary: &str, context: &str, system: &str) -> RunRequest {
|
||||
let prompt = format!(
|
||||
"# Pass: {kind}\n\n## Context\n\n{context}\n\n## Input\n\n{primary}",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue