Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions apps/web/src/components/agents/Dream.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Moon } from "lucide-react";
import { useCallback, useState } from "react";

import { useI18n } from "../../i18n/context";
import { useEngine } from "../../lib/engine-context";
import { useErrorText } from "../../lib/errors";
import { readJson } from "../../lib/errors";
import { url } from "../../lib/library";
import { useLoad } from "../../lib/use-load";
import { Button } from "../ui";

interface State {
dream: boolean;
hour: number;
last: {
day: string;
agents: { agent: string; before: number; after: number; refused?: string }[];
} | null;
}

/**
* Let the agents sleep on it.
*
* Memory only ever grew: the same fact written three ways, a correction sitting above the thing it
* corrected, a note about a project that finished in March — all of it in every prompt, forever.
* Once a night the agent re-reads what it knows and writes back a shorter version that says the
* same things.
*
* Off unless asked, because it is a language-model call per agent per day, and it says what it did:
* how many lines went in, how many came out, or why the night was thrown away. The previous memory
* is kept in `DREAMS.md` beside the agent, so a bad night is one file to open and one block to
* paste back.
*/
export function DreamPanel() {
const { handshake } = useEngine();
const { t } = useI18n();
const say = useErrorText();
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);

const state = useLoad(
useCallback(
async () => readJson<State>(await fetch(url(handshake, "/agent/dream"))),
[handshake],
),
[handshake],
);

const send = async (body: Record<string, unknown>) => {
setBusy(true);
setError(null);
try {
await readJson(
await fetch(url(handshake, "/agent/dream"), {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
}),
);
state.reload();
} catch (e) {
setError(say(e));
} finally {
setBusy(false);
}
};

const on = state.data?.dream ?? false;
const last = state.data?.last ?? null;

return (
<section
className="border-line bg-bg-soft mt-6 rounded-[var(--radius-panel)] border p-4"
data-testid="dream"
>
<div className="flex flex-wrap items-center gap-2">
<Moon className="text-ai size-4 shrink-0" aria-hidden="true" />
<h2 className="flex-1 text-sm font-semibold">{t("agents.dream")}</h2>
<label className="text-meta flex items-center gap-1.5">
<input
type="checkbox"
checked={on}
disabled={busy}
onChange={(e) => void send({ dream: e.target.checked })}
className="accent-[var(--color-accent)]"
/>
{t("agents.dream_nightly", { hour: String(state.data?.hour ?? 3) })}
</label>
<Button variant="ghost" size="sm" disabled={busy} onClick={() => void send({ now: true })}>
{busy ? t("agents.dream_working") : t("agents.dream_now")}
</Button>
</div>
<p className="text-fg-faint text-micro mt-1">{t("agents.dream_hint")}</p>

{error && (
<p role="alert" className="text-danger mt-2 text-sm">
{error}
</p>
)}

{last && (
<ul className="text-meta mt-3 space-y-1" data-testid="dream-last">
{last.agents.map((one) => (
<li key={one.agent} className="text-fg-dim">
<span className="font-medium">{one.agent}</span>
{" — "}
{one.refused
? t("agents.dream_refused", { why: one.refused })
: t("agents.dream_shrank", {
before: String(one.before),
after: String(one.after),
})}
</li>
))}
{last.agents.length === 0 && <li className="text-fg-faint">{t("agents.dream_none")}</li>}
</ul>
)}
</section>
);
}
10 changes: 9 additions & 1 deletion apps/web/src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,15 @@
"task_count_one": "{count} open task",
"task_count_other": "{count} open tasks",
"dangling": "{from} can hand work to “{to}”, but no agent by that name exists.",
"unreadable": "Could not read {path}: {reason}"
"unreadable": "Could not read {path}: {reason}",
"dream": "Sleep on it",
"dream_nightly": "Every night, after {hour}:00",
"dream_now": "Do it now",
"dream_working": "Thinking…",
"dream_hint": "The agent re-reads its own memory and writes it shorter: merge duplicates, keep the latest correction, drop what is finished. It adds nothing. The old version stays in DREAMS.md.",
"dream_refused": "skipped — {why}",
"dream_shrank": "{before} → {after} lines",
"dream_none": "No night yet."
},
"finder": {
"title": "Find",
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/i18n/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,15 @@
"task_count_one": "未完了{count}件",
"task_count_other": "未完了{count}件",
"dangling": "{from}は「{to}」に仕事を渡せますが、その名前のエージェントは存在しません。",
"unreadable": "{path}を読めませんでした:{reason}"
"unreadable": "{path}を読めませんでした:{reason}",
"dream": "寝かせて整理",
"dream_nightly": "毎晩 {hour} 時以降",
"dream_now": "今すぐ整理",
"dream_working": "整理中…",
"dream_hint": "エージェントが自分の記憶を読み直して短くします:重複をまとめ、最新の訂正を残し、終わったことを落とす。新しいことは足しません。元の版は DREAMS.md に残ります。",
"dream_refused": "見送り — {why}",
"dream_shrank": "{before} → {after} 行",
"dream_none": "まだ一度もありません。"
},
"finder": {
"title": "絞り込み",
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/i18n/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,15 @@
"task_count_one": "{count} việc treo",
"task_count_other": "{count} việc treo",
"dangling": "{from} có thể giao việc cho “{to}”, nhưng không có agent nào tên đó.",
"unreadable": "Không đọc được {path}: {reason}"
"unreadable": "Không đọc được {path}: {reason}",
"dream": "Ngủ & ôn lại",
"dream_nightly": "Mỗi đêm, sau {hour} giờ",
"dream_now": "Ôn ngay",
"dream_working": "Đang ôn…",
"dream_hint": "Agent đọc lại trí nhớ của nó rồi viết gọn: gộp trùng, giữ bản sửa mới nhất, bỏ việc đã xong. Không thêm điều gì mới. Bản cũ vẫn nằm trong DREAMS.md.",
"dream_refused": "bỏ qua — {why}",
"dream_shrank": "{before} → {after} dòng",
"dream_none": "Chưa ôn lần nào."
},
"finder": {
"title": "Tìm",
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/i18n/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,15 @@
"task_count_one": "{count}项未完成",
"task_count_other": "{count}项未完成",
"dangling": "{from}可以把活交给“{to}”,但没有叫这个名字的智能体。",
"unreadable": "读不了{path}:{reason}"
"unreadable": "读不了{path}:{reason}",
"dream": "睡一觉再整理",
"dream_nightly": "每晚 {hour} 点后",
"dream_now": "立即整理",
"dream_working": "整理中…",
"dream_hint": "助理重读自己的记忆并写得更短:合并重复、保留最新的更正、去掉已完成的。不会新增任何内容。旧版本保留在 DREAMS.md。",
"dream_refused": "跳过 — {why}",
"dream_shrank": "{before} → {after} 行",
"dream_none": "还没有整理过。"
},
"finder": {
"title": "筛选",
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/screens/AgentsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DreamPanel } from "../components/agents/Dream";
import { AnimatePresence, motion } from "motion/react";
import { useCallback, useMemo, useRef, useState } from "react";

Expand Down Expand Up @@ -151,6 +152,7 @@ export function AgentsScreen() {
return (
<Page title={t("agents.title")} subtitle={t("agents.subtitle")}>
<PageGlow />
<DreamPanel />

{error && (
<p className="border-rec/30 bg-rec-soft text-rec text-meta rounded-lg border px-3 py-2">
Expand Down
20 changes: 20 additions & 0 deletions crates/summo-agent/src/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,26 @@ pub fn remember(path: &Path, today: &str, text: &str) -> Result<bool> {
Ok(true)
}

/// Replace the whole list, keeping the file's shape.
///
/// For consolidation — see `summo_engine::dream`, the one caller. Deliberately not exposed as a
/// tool: an agent that could rewrite its own memory wholesale mid-run could erase a correction it
/// had just been given, and the value of memory is that the user can rely on what they put in it.
/// A night's consolidation is a different act, and it archives what it replaces before it writes.
pub fn replace(path: &Path, facts: &[String], today: &str) -> Result<()> {
let mut out = String::from("# Memory\n\n");
for text in facts.iter().take(MAX_LINES) {
let text = text.trim();
if !text.is_empty() {
out.push_str(&format!("- {today} — {text}\n"));
}
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| Error::io(parent, e))?;
}
std::fs::write(path, out).map_err(|e| Error::io(path, e))
}

/// Forget one fact, by exact text. What makes the memory the user's rather than the agent's.
pub fn forget(path: &Path, text: &str) -> Result<bool> {
let facts = load(path);
Expand Down
25 changes: 25 additions & 0 deletions crates/summo-core/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,26 @@ pub struct Settings {
pub llm: Llm,
pub storage: Storage,
pub interface: Interface,
pub agents: Agents,
/// Fields this build does not know about, kept so a downgrade does not erase them.
#[serde(flatten)]
pub unknown: BTreeMap<String, serde_json::Value>,
}

/// What the agents do when nobody is asking.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Agents {
/// Once a day, let each agent re-read and shorten its own memory.
///
/// Off by default. It costs a language-model call per agent per day, and on the first day
/// there is nothing to consolidate — a memory of four lines is already as short as it gets.
pub dream: bool,
/// Hour, local, after which it may happen. Late, because it is unattended work on a file that
/// steers every later answer, and the user should be asleep rather than mid-sentence.
pub dream_hour: u8,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct Recording {
Expand Down Expand Up @@ -168,6 +183,15 @@ pub struct Interface {
pub show_performance: bool,
}

impl Default for Agents {
fn default() -> Self {
Self {
dream: false,
dream_hour: 3,
}
}
}

impl Default for Settings {
fn default() -> Self {
Self {
Expand All @@ -177,6 +201,7 @@ impl Default for Settings {
llm: Llm::default(),
storage: Storage::default(),
interface: Interface::default(),
agents: Agents::default(),
unknown: BTreeMap::new(),
}
}
Expand Down
Loading
Loading