diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..b05a0a8 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "summary", + "runtimeExecutable": "python", + "runtimeArgs": ["-m", "http.server", "4321"], + "port": 4321 + } + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..20f03ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Secrets — never commit +.env +.env.* + +# Provisioned resource IDs (created at runtime by create_agent.py) +.agent_id +.environment_id +.curator_agent_id +.memory_store_id +.memory_store_* + +# Session outputs / snapshots +outputs/ + +# Python +__pycache__/ +*.pyc +.venv/ +venv/ diff --git a/build_deck.py b/build_deck.py new file mode 100644 index 0000000..493a08a --- /dev/null +++ b/build_deck.py @@ -0,0 +1,575 @@ +"""Generate the Card-D demo PowerPoint from deck_content.py. + +Pure python-pptx — native, editable shapes/charts, no headless renderer. +Run: ``python build_deck.py`` -> ``outputs/total-recall-card-d.pptx`` +""" + +import os + +from pptx import Presentation +from pptx.util import Inches, Pt, Emu +from pptx.dml.color import RGBColor +from pptx.enum.text import PP_ALIGN, MSO_ANCHOR +from pptx.enum.shapes import MSO_SHAPE +from pptx.chart.data import CategoryChartData +from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION + +import deck_content as C + +# 16:9 canvas +SLIDE_W = Inches(13.333) +SLIDE_H = Inches(7.5) +MARGIN = Inches(0.6) +OUT_PATH = os.path.join("outputs", "total-recall-card-d.pptx") + + +def rgb(name_or_tuple): + """Accept a BRAND key or an (r,g,b) tuple, return an RGBColor.""" + val = C.BRAND[name_or_tuple] if isinstance(name_or_tuple, str) else name_or_tuple + return RGBColor(*val) + + +# --- low-level builders ---------------------------------------------------- +def _blank_slide(prs): + return prs.slides.add_slide(prs.slide_layouts[6]) # fully blank layout + + +def _textbox(slide, left, top, width, height): + box = slide.shapes.add_textbox(left, top, width, height) + tf = box.text_frame + tf.word_wrap = True + return box, tf + + +def _set_run(run, text, size, color, bold=False, italic=False, font=None): + run.text = text + run.font.size = Pt(size) + run.font.bold = bold + run.font.italic = italic + run.font.name = font or C.BRAND["body_font"] + run.font.color.rgb = rgb(color) + + +def _fill(shape, color): + shape.fill.solid() + shape.fill.fore_color.rgb = rgb(color) + shape.line.fill.background() + + +def _bg(slide, color="white"): + rect = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_W, SLIDE_H) + _fill(rect, color) + rect.shadow.inherit = False + # send to back so other shapes render on top + sp = rect._element + sp.getparent().remove(sp) + slide.shapes._spTree.insert(2, sp) + return rect + + +def _accent_bar(slide): + bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SLIDE_W, Inches(0.18)) + _fill(bar, "accent") + bar.shadow.inherit = False + + +def _heading(slide, title, kicker=None): + """Standard slide heading; returns the bottom Y to start body content.""" + _accent_bar(slide) + top = Inches(0.45) + if kicker: + _, tf = _textbox(slide, MARGIN, top, SLIDE_W - 2 * MARGIN, Inches(0.4)) + _set_run(tf.paragraphs[0].add_run(), kicker.upper(), 13, "accent", + bold=True, font=C.BRAND["title_font"]) + top = Inches(0.85) + box, tf = _textbox(slide, MARGIN, top, SLIDE_W - 2 * MARGIN, Inches(0.9)) + _set_run(tf.paragraphs[0].add_run(), title, 30, "ink", bold=True, + font=C.BRAND["title_font"]) + return top + Inches(1.0) + + +def _bullets(slide, items, left, top, width, height, size=16, + color="ink", bullet_color="accent", gap=8): + """items: list of strings or (text, bold) tuples.""" + _, tf = _textbox(slide, left, top, width, height) + for i, item in enumerate(items): + text, bold = (item, False) if isinstance(item, str) else item + p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() + p.space_after = Pt(gap) + r = p.add_run() + _set_run(r, "• ", size, bullet_color, bold=True) + r2 = p.add_run() + _set_run(r2, text, size, color, bold=bold) + return tf + + +def _panel(slide, left, top, width, height, fill="surface"): + panel = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height) + _fill(panel, fill) + panel.shadow.inherit = False + return panel + + +# --- slide templates ------------------------------------------------------- +def title_slide(prs): + s = _blank_slide(prs) + _bg(s, "ink") + band = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, Inches(4.85), SLIDE_W, Inches(0.12)) + _fill(band, "accent") + _, tf = _textbox(s, MARGIN, Inches(2.1), SLIDE_W - 2 * MARGIN, Inches(1.6)) + _set_run(tf.paragraphs[0].add_run(), "TOTAL RECALL", 54, "white", bold=True, + font=C.BRAND["title_font"]) + p = tf.add_paragraph() + _set_run(p.add_run(), "An agent that visibly gets sharper across sessions", + 24, "accent", font=C.BRAND["title_font"]) + _, tf2 = _textbox(s, MARGIN, Inches(5.1), SLIDE_W - 2 * MARGIN, Inches(1.4)) + _set_run(tf2.paragraphs[0].add_run(), + "Card D — Sales Engineer for Helios", 20, "white", bold=True) + p2 = tf2.add_paragraph() + _set_run(p2.add_run(), + "Claude Managed Agents + the Memory tool • Prospect: Northwind Bank", + 14, "muted") + return s + + +def section_quote_slide(prs, kicker, big_text, sub=None, fill="white", + big_color="ink", accent_word_color="accent"): + s = _blank_slide(prs) + _bg(s, fill) + _accent_bar(s) + _, tf = _textbox(s, MARGIN, Inches(0.6), SLIDE_W - 2 * MARGIN, Inches(0.5)) + _set_run(tf.paragraphs[0].add_run(), kicker.upper(), 14, "accent", bold=True, + font=C.BRAND["title_font"]) + box, tf2 = _textbox(s, MARGIN, Inches(2.3), SLIDE_W - 2 * MARGIN, Inches(2.6)) + tf2.word_wrap = True + _set_run(tf2.paragraphs[0].add_run(), big_text, 34, big_color, bold=True, + font=C.BRAND["title_font"]) + if sub: + _, tf3 = _textbox(s, MARGIN, Inches(5.0), SLIDE_W - 2 * MARGIN, Inches(1.6)) + _set_run(tf3.paragraphs[0].add_run(), sub, 18, "muted") + return s + + +def bullets_slide(prs, title, items, kicker=None, intro=None, size=18): + s = _blank_slide(prs) + _bg(s) + top = _heading(s, title, kicker) + if intro: + _, tf = _textbox(s, MARGIN, top, SLIDE_W - 2 * MARGIN, Inches(0.7)) + _set_run(tf.paragraphs[0].add_run(), intro, 16, "muted", italic=True) + top += Inches(0.8) + _bullets(s, items, MARGIN, top, SLIDE_W - 2 * MARGIN, + SLIDE_H - top - Inches(0.4), size=size) + return s + + +def why_memory_slide(prs): + s = _blank_slide(prs) + _bg(s) + _heading(s, "Memory is not a vector database", kicker="Why this matters") + # left: the misconception, right: what it really is + col_w = (SLIDE_W - 2 * MARGIN - Inches(0.4)) / 2 + lp = _panel(s, MARGIN, Inches(2.0), col_w, Inches(4.6), "surface") + _, tf = _textbox(s, MARGIN + Inches(0.3), Inches(2.3), col_w - Inches(0.6), Inches(4.0)) + _set_run(tf.paragraphs[0].add_run(), "What clients think it is", 18, "muted", bold=True) + p = tf.add_paragraph(); p.space_before = Pt(10) + _set_run(p.add_run(), "“A vector database for documents.”", 20, "ink", italic=True) + p = tf.add_paragraph(); p.space_before = Pt(10) + _set_run(p.add_run(), "Store everything, retrieve by similarity, hope the right chunk comes back.", + 15, "muted") + + rx = MARGIN + col_w + Inches(0.4) + rp = _panel(s, rx, Inches(2.0), col_w, Inches(4.6), "ink") + _, tf2 = _textbox(s, rx + Inches(0.3), Inches(2.3), col_w - Inches(0.6), Inches(4.0)) + _set_run(tf2.paragraphs[0].add_run(), "What it actually is", 18, "accent", bold=True) + p = tf2.add_paragraph(); p.space_before = Pt(10) + _set_run(p.add_run(), + "The agent decides what to remember, what to forget, and what to update when it learns something new.", + 20, "white", bold=True) + for line in ["Remember the durable facts.", "Forget the noise.", + "Update when new info contradicts the old."]: + p = tf2.add_paragraph(); p.space_before = Pt(8) + r = p.add_run(); _set_run(r, "• ", 16, "accent", bold=True) + r2 = p.add_run(); _set_run(r2, line, 16, "white") + return s + + +def scenario_slide(prs): + s = _blank_slide(prs) + _bg(s) + top = _heading(s, "The scenario", kicker="Card D — Helios Sales Engineer") + _, tf = _textbox(s, MARGIN, top, SLIDE_W - 2 * MARGIN, Inches(1.0)) + _set_run(tf.paragraphs[0].add_run(), C.SCENARIO["product"] + " • ", 16, "ink", bold=True) + _set_run(tf.paragraphs[0].add_run(), "Prospect: " + C.SCENARIO["prospect"], 16, "muted") + p = tf.add_paragraph(); p.space_before = Pt(6) + _set_run(p.add_run(), C.SCENARIO["persona"], 14, "muted", italic=True) + + col_w = (SLIDE_W - 2 * MARGIN - Inches(0.4)) / 2 + rtop = top + Inches(1.5) + ph = Inches(3.2) + _panel(s, MARGIN, rtop, col_w, ph, "surface") + _, t1 = _textbox(s, MARGIN + Inches(0.25), rtop + Inches(0.15), col_w - Inches(0.5), ph - Inches(0.3)) + _set_run(t1.paragraphs[0].add_run(), "ROUND 1 — what we know", 14, "session1", bold=True) + for name, desc in C.SCENARIO["round1_docs"]: + p = t1.add_paragraph(); p.space_before = Pt(8) + _set_run(p.add_run(), name + " ", 13, "ink", bold=True) + p2 = t1.add_paragraph() + _set_run(p2.add_run(), desc, 11.5, "muted") + + rx = MARGIN + col_w + Inches(0.4) + _panel(s, rx, rtop, col_w, ph, "surface") + _, t2 = _textbox(s, rx + Inches(0.25), rtop + Inches(0.15), col_w - Inches(0.5), ph - Inches(0.3)) + _set_run(t2.paragraphs[0].add_run(), "ROUND 2 — what's new (contradicts)", 14, "session2", bold=True) + for name, desc in C.SCENARIO["round2_docs"]: + p = t2.add_paragraph(); p.space_before = Pt(8) + _set_run(p.add_run(), name + " ", 13, "ink", bold=True) + p2 = t2.add_paragraph() + _set_run(p2.add_run(), desc, 11.5, "muted") + return s + + +def _arrow(slide, x1, y1, x2, y2, color="muted"): + conn = slide.shapes.add_connector(2, x1, y1, x2, y2) # straight connector + conn.line.color.rgb = rgb(color) + conn.line.width = Pt(2.25) + try: + conn.line.headEnd = "none" + except Exception: + pass + return conn + + +def architecture_slide(prs): + s = _blank_slide(prs) + _bg(s) + _heading(s, "How it works", kicker="Architecture") + + def box(left, top, w, h, label, sub, fill, txt="white"): + shp = _panel(s, left, top, w, h, fill) + _, tf = _textbox(s, left + Inches(0.12), top + Inches(0.12), w - Inches(0.24), h - Inches(0.24)) + tf.word_wrap = True + _set_run(tf.paragraphs[0].add_run(), label, 15, txt, bold=True) + if sub: + p = tf.add_paragraph() + _set_run(p.add_run(), sub, 11, txt) + return shp + + row_y = Inches(2.4) + bw, bh = Inches(2.7), Inches(1.5) + gap = Inches(0.55) + x0 = MARGIN + Inches(0.1) + b1 = box(x0, row_y, bw, bh, "Sales Engineer", "asks the question", "session1") + b2 = box(x0 + (bw + gap), row_y, bw, bh, "Managed Agent", "Claude + Memory tool", "accent") + b3 = box(x0 + 2 * (bw + gap), row_y, bw, bh, "Memory Store", + "path-organized\n/customers /objections /competitive /pitch", "ink") + _arrow(s, x0 + bw, row_y + bh / 2, x0 + bw + gap, row_y + bh / 2, "muted") + _arrow(s, x0 + 2 * bw + gap, row_y + bh / 2, x0 + 2 * (bw + gap), row_y + bh / 2, "muted") + + # docs feeding in + docs = box(x0 + (bw + gap), row_y + bh + Inches(0.7), bw, Inches(1.1), + "Files API → round1 / round2 docs", "uploaded per session", "session2") + _arrow(s, x0 + (bw + gap) + bw / 2, row_y + bh + Inches(0.7), + x0 + (bw + gap) + bw / 2, row_y + bh, "muted") + + # footnote: two kinds of memory + _, tf = _textbox(s, MARGIN, Inches(6.0), SLIDE_W - 2 * MARGIN, Inches(1.1)) + _set_run(tf.paragraphs[0].add_run(), "Two kinds of memory: ", 14, "accent", bold=True) + _set_run(tf.paragraphs[0].add_run(), + "in-session (the context window + built-in compaction) vs across-session " + "(the persistent Memory Store). Sessions come and go; the store endures.", + 14, "muted") + return s + + +def session_answer_slide(prs, n, data, color): + s = _blank_slide(prs) + _bg(s) + top = _heading(s, "Session %d — the answer" % n, + kicker="Q: " + C.TEST_QUESTION) + # preface panel + _panel(s, MARGIN, top, SLIDE_W - 2 * MARGIN, Inches(0.85), "surface") + _, tf = _textbox(s, MARGIN + Inches(0.2), top + Inches(0.1), + SLIDE_W - 2 * MARGIN - Inches(0.4), Inches(0.65)) + _set_run(tf.paragraphs[0].add_run(), data["preface"], 13, "muted", italic=True) + # headline + htop = top + Inches(1.05) + _, tf2 = _textbox(s, MARGIN, htop, SLIDE_W - 2 * MARGIN, Inches(0.7)) + _set_run(tf2.paragraphs[0].add_run(), data["headline"], 20, color, bold=True) + # bullets + _bullets(s, data["bullets"], MARGIN, htop + Inches(0.8), + SLIDE_W - 2 * MARGIN, Inches(2.9), size=14, bullet_color=color) + # gap note + _, tf3 = _textbox(s, MARGIN, Inches(6.7), SLIDE_W - 2 * MARGIN, Inches(0.6)) + _set_run(tf3.paragraphs[0].add_run(), "▸ " + data["gap"], 13, "accent_dark", bold=True) + return s + + +def between_calls_slide(prs): + items = [] + for name, desc in C.SCENARIO["round2_docs"]: + items.append((name, True)) + items.append(desc) + s = _blank_slide(prs) + _bg(s) + top = _heading(s, "Between the calls", kicker="Two new round-2 inputs") + col_w = (SLIDE_W - 2 * MARGIN - Inches(0.4)) / 2 + ph = Inches(3.6) + titles = ["NEW OBJECTION (latest call)", "COMPETITIVE UPDATE (from product)"] + for i, (name, desc) in enumerate(C.SCENARIO["round2_docs"]): + x = MARGIN + i * (col_w + Inches(0.4)) + _panel(s, x, top, col_w, ph, "surface") + _, tf = _textbox(s, x + Inches(0.25), top + Inches(0.2), col_w - Inches(0.5), ph - Inches(0.4)) + _set_run(tf.paragraphs[0].add_run(), titles[i], 14, "session2", bold=True) + p = tf.add_paragraph(); p.space_before = Pt(8) + _set_run(p.add_run(), name, 14, "ink", bold=True) + p2 = tf.add_paragraph(); p2.space_before = Pt(8) + _set_run(p2.add_run(), desc, 15, "ink") + _, tf = _textbox(s, MARGIN, top + ph + Inches(0.25), SLIDE_W - 2 * MARGIN, Inches(0.7)) + _set_run(tf.paragraphs[0].add_run(), + "These contradict / extend round 1 — the agent must reconcile, not just append.", + 15, "muted", italic=True) + return s + + +def comparison_slide(prs): + s = _blank_slide(prs) + _bg(s) + _heading(s, "Same question, sharper answer", kicker="The headline") + col_w = (SLIDE_W - 2 * MARGIN - Inches(0.4)) / 2 + top = Inches(2.0) + ph = Inches(4.9) + # session 1 column + _panel(s, MARGIN, top, col_w, ph, "surface") + hdr1 = s.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, MARGIN, top, col_w, Inches(0.6)) + _fill(hdr1, "session1") + _, h1 = _textbox(s, MARGIN + Inches(0.2), top + Inches(0.06), col_w - Inches(0.4), Inches(0.5)) + _set_run(h1.paragraphs[0].add_run(), "SESSION 1 — cold start", 15, "white", bold=True) + _bullets(s, [C.SESSION_1_ANSWER["headline"]] + C.SESSION_1_ANSWER["bullets"][:4], + MARGIN + Inches(0.2), top + Inches(0.8), col_w - Inches(0.4), ph - Inches(1.0), + size=12.5, bullet_color="session1") + + rx = MARGIN + col_w + Inches(0.4) + _panel(s, rx, top, col_w, ph, "surface") + hdr2 = s.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, rx, top, col_w, Inches(0.6)) + _fill(hdr2, "session2") + _, h2 = _textbox(s, rx + Inches(0.2), top + Inches(0.06), col_w - Inches(0.4), Inches(0.5)) + _set_run(h2.paragraphs[0].add_run(), "SESSION 2 — with memory + new context", 15, "white", bold=True) + _bullets(s, [C.SESSION_2_ANSWER["headline"]] + C.SESSION_2_ANSWER["bullets"][:4], + rx + Inches(0.2), top + Inches(0.8), col_w - Inches(0.4), ph - Inches(1.0), + size=12.5, bullet_color="session2") + return s + + +def memory_diff_slide(prs): + s = _blank_slide(prs) + _bg(s) + top = _heading(s, "What the agent learned between calls", + kicker="S8 — Memory diff") + rows = [] + for path, content in C.MEMORY_DIFF["added"]: + rows.append(("ADDED", path, content, "added")) + for path, content in C.MEMORY_DIFF["changed"]: + rows.append(("CHANGED", path, content, "changed")) + n_unchanged = len(C.MEMORY_DIFF["unchanged"]) + + n = len(rows) + 1 # +header + table_w = SLIDE_W - 2 * MARGIN + th = Inches(4.5) + tbl_shape = s.shapes.add_table(n + 1, 3, MARGIN, top, table_w, th) + table = tbl_shape.table + table.columns[0].width = Inches(1.4) + table.columns[1].width = Inches(3.6) + table.columns[2].width = table_w - Inches(5.0) + + def cell(r, c, text, color="ink", bold=False, size=12, fill=None): + cl = table.cell(r, c) + cl.vertical_anchor = MSO_ANCHOR.MIDDLE + if fill: + cl.fill.solid(); cl.fill.fore_color.rgb = rgb(fill) + else: + cl.fill.solid(); cl.fill.fore_color.rgb = rgb("white") + tf = cl.text_frame; tf.word_wrap = True + tf.paragraphs[0].clear() + _set_run(tf.paragraphs[0].add_run(), text, size, color, bold=bold) + + cell(0, 0, "CHANGE", "white", True, 12, "ink") + cell(0, 1, "MEMORY PATH", "white", True, 12, "ink") + cell(0, 2, "CONTENT", "white", True, 12, "ink") + for i, (tag, path, content, ckey) in enumerate(rows, start=1): + cell(i, 0, tag, ckey, True, 11) + cell(i, 1, path, "ink", False, 11) + cell(i, 2, content, "muted", False, 11) + # final summary row + cell(n, 0, "UNCHANGED", "unchanged", True, 11) + cell(n, 1, "%d entries" % n_unchanged, "unchanged", False, 11) + cell(n, 2, "carried over from session 1 untouched", "muted", True, 11) + + _, tf = _textbox(s, MARGIN, top + th + Inches(0.15), SLIDE_W - 2 * MARGIN, Inches(0.5)) + _set_run(tf.paragraphs[0].add_run(), + "“Here's exactly what the agent learned” — added the failover objection, " + "competitive intel, and the new CISO; updated residency, cost, and the pitch sequence.", + 13, "muted", italic=True) + return s + + +def memory_growth_slide(prs): + s = _blank_slide(prs) + _bg(s) + top = _heading(s, "Memory grows where it should", kicker="Facts remembered per category") + chart_data = CategoryChartData() + chart_data.categories = [c for c, _, _ in C.MEMORY_GROWTH] + chart_data.add_series("After session 1", tuple(a for _, a, _ in C.MEMORY_GROWTH)) + chart_data.add_series("After session 2", tuple(b for _, _, b in C.MEMORY_GROWTH)) + gf = s.shapes.add_chart( + XL_CHART_TYPE.COLUMN_CLUSTERED, MARGIN, top, + SLIDE_W - 2 * MARGIN, Inches(4.4), chart_data) + chart = gf.chart + chart.has_legend = True + chart.legend.position = XL_LEGEND_POSITION.BOTTOM + chart.legend.include_in_layout = False + plot = chart.plots[0] + plot.series[0].format.fill.solid() + plot.series[0].format.fill.fore_color.rgb = rgb("session1") + plot.series[1].format.fill.solid() + plot.series[1].format.fill.fore_color.rgb = rgb("session2") + _, tf = _textbox(s, MARGIN, top + Inches(4.5), SLIDE_W - 2 * MARGIN, Inches(0.5)) + _set_run(tf.paragraphs[0].add_run(), + "New competitive intel and a new objection appear; pitch strategy is one " + "entry that gets rewritten in place rather than duplicated.", + 13, "muted", italic=True) + return s + + +def business_value_slide(prs): + s = _blank_slide(prs) + _bg(s) + top = _heading(s, "Why enterprise buyers care", kicker="The questions every client asks") + y = top + rh = Inches(0.85) + for q, a in C.BUSINESS_VALUE: + _panel(s, MARGIN, y, SLIDE_W - 2 * MARGIN, rh - Inches(0.12), "surface") + _, tf = _textbox(s, MARGIN + Inches(0.25), y + Inches(0.02), + SLIDE_W - 2 * MARGIN - Inches(0.5), rh - Inches(0.2)) + _set_run(tf.paragraphs[0].add_run(), q + " ", 15, "ink", bold=True) + _set_run(tf.paragraphs[0].add_run(), a, 14, "muted") + y += rh + return s + + +def stretch_overview_slide(prs): + s = _blank_slide(prs) + _bg(s) + _heading(s, "Nine ways to make memory real", kicker="Stretch goals S1–S9") + col_w = (SLIDE_W - 2 * MARGIN - Inches(0.6)) / 2 + positions = [(MARGIN, Inches(1.9)), (MARGIN + col_w + Inches(0.6), Inches(1.9)), + (MARGIN, Inches(4.45)), (MARGIN + col_w + Inches(0.6), Inches(4.45))] + ph = Inches(2.35) + for (tier_title, goals), (x, y) in zip(C.STRETCH_TIERS, positions): + _panel(s, x, y, col_w, ph, "surface") + _, tf = _textbox(s, x + Inches(0.25), y + Inches(0.15), col_w - Inches(0.5), ph - Inches(0.3)) + _set_run(tf.paragraphs[0].add_run(), tier_title, 14, "accent_dark", bold=True) + for gid, gtitle, _what, _why in goals: + p = tf.add_paragraph(); p.space_before = Pt(6) + _set_run(p.add_run(), gid + " ", 13, "accent", bold=True) + _set_run(p.add_run(), gtitle, 13, "ink", bold=True) + return s + + +def tier_detail_slide(prs, tier_index): + tier_title, goals = C.STRETCH_TIERS[tier_index] + s = _blank_slide(prs) + _bg(s) + top = _heading(s, tier_title, kicker="Stretch goals") + # one panel per goal stacked + avail = SLIDE_H - top - Inches(0.4) + gh = (avail - Inches(0.3) * (len(goals) - 1)) / len(goals) + y = top + for gid, gtitle, what, why in goals: + _panel(s, MARGIN, y, SLIDE_W - 2 * MARGIN, gh, "surface") + _, tf = _textbox(s, MARGIN + Inches(0.3), y + Inches(0.15), + SLIDE_W - 2 * MARGIN - Inches(0.6), gh - Inches(0.3)) + _set_run(tf.paragraphs[0].add_run(), gid + " — " + gtitle, 17, "accent_dark", bold=True) + p = tf.add_paragraph(); p.space_before = Pt(5) + _set_run(p.add_run(), what, 13.5, "ink") + p2 = tf.add_paragraph(); p2.space_before = Pt(4) + _set_run(p2.add_run(), "Why it lands: ", 13, "accent", bold=True) + _set_run(p2.add_run(), why, 13, "muted", italic=True) + y += gh + Inches(0.3) + return s + + +def how_to_run_slide(prs): + s = _blank_slide(prs) + _bg(s) + top = _heading(s, "Run it end-to-end", kicker="Verification") + _panel(s, MARGIN, top, SLIDE_W - 2 * MARGIN, Inches(4.6), "ink") + _, tf = _textbox(s, MARGIN + Inches(0.35), top + Inches(0.25), + SLIDE_W - 2 * MARGIN - Inches(0.7), Inches(4.1)) + for i, line in enumerate(C.HOW_TO_RUN): + p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() + p.space_after = Pt(8) + _set_run(p.add_run(), "$ ", 14, "accent", bold=True, font="Consolas") + _set_run(p.add_run(), line, 13.5, "white", font="Consolas") + return s + + +def closing_slide(prs): + s = _blank_slide(prs) + _bg(s, "ink") + band = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, Inches(3.4), SLIDE_W, Inches(0.1)) + _fill(band, "accent") + _, tf = _textbox(s, MARGIN, Inches(2.2), SLIDE_W - 2 * MARGIN, Inches(1.2)) + _set_run(tf.paragraphs[0].add_run(), + "Same question. Two sessions. A visibly sharper answer.", + 30, "white", bold=True, font=C.BRAND["title_font"]) + _, tf2 = _textbox(s, MARGIN, Inches(3.7), SLIDE_W - 2 * MARGIN, Inches(1.4)) + _set_run(tf2.paragraphs[0].add_run(), + "That's institutional memory: the agent decides what to keep, what to drop, " + "and what to rewrite when the world changes.", + 18, "muted") + p = tf2.add_paragraph(); p.space_before = Pt(14) + _set_run(p.add_run(), "Regenerate this deck anytime: python build_deck.py", + 14, "accent", font="Consolas") + return s + + +def build(): + prs = Presentation() + prs.slide_width = SLIDE_W + prs.slide_height = SLIDE_H + + title_slide(prs) # 1 + why_memory_slide(prs) # 2 + section_quote_slide(prs, "The demo in one line", + "Same question, asked twice. The second answer is sharper " + "because the agent remembered — and reconciled what changed.", + sub="No new prompt engineering between calls. Just memory.") # 3 + scenario_slide(prs) # 4 + architecture_slide(prs) # 5 + section_quote_slide(prs, "The test question (both sessions)", + "“" + C.TEST_QUESTION + "”", + sub="Asked identically in session 1 and session 2.", + big_color="accent") # 6 + session_answer_slide(prs, 1, C.SESSION_1_ANSWER, "session1") # 7 + between_calls_slide(prs) # 8 + session_answer_slide(prs, 2, C.SESSION_2_ANSWER, "session2") # 9 + comparison_slide(prs) # 10 + memory_diff_slide(prs) # 11 + memory_growth_slide(prs) # 12 + business_value_slide(prs) # 13 + stretch_overview_slide(prs) # 14 + tier_detail_slide(prs, 0) # 15 + tier_detail_slide(prs, 1) # 16 + tier_detail_slide(prs, 2) # 17 + tier_detail_slide(prs, 3) # 18 + how_to_run_slide(prs) # 19 + closing_slide(prs) # 20 + + os.makedirs("outputs", exist_ok=True) + prs.save(OUT_PATH) + return len(prs.slides._sldIdLst) + + +if __name__ == "__main__": + count = build() + print("Wrote %s (%d slides)" % (OUT_PATH, count)) diff --git a/build_exec_deck.py b/build_exec_deck.py new file mode 100644 index 0000000..76a4d91 --- /dev/null +++ b/build_exec_deck.py @@ -0,0 +1,112 @@ +"""Generate the 5-slide *executive summary* of the Card-D demo. + +Reuses the slide builders, helpers, and content from build_deck.py — this file +only chooses which slides make the executive cut and adds a tailored title and +closing/ask. Run: ``python build_exec_deck.py`` + -> ``outputs/total-recall-card-d-exec.pptx`` +""" + +import os + +from pptx import Presentation +from pptx.util import Inches, Pt +from pptx.enum.shapes import MSO_SHAPE + +import deck_content as C +import build_deck as bd + +OUT_PATH = os.path.join("outputs", "total-recall-card-d-exec.pptx") + + +def exec_title_slide(prs): + s = bd._blank_slide(prs) + bd._bg(s, "ink") + band = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, Inches(4.7), bd.SLIDE_W, Inches(0.12)) + bd._fill(band, "accent") + _, tf = bd._textbox(s, bd.MARGIN, Inches(1.7), bd.SLIDE_W - 2 * bd.MARGIN, Inches(1.7)) + bd._set_run(tf.paragraphs[0].add_run(), "TOTAL RECALL", 50, "white", bold=True, + font=C.BRAND["title_font"]) + p = tf.add_paragraph() + bd._set_run(p.add_run(), "Executive summary", 24, "accent", + font=C.BRAND["title_font"]) + _, tf2 = bd._textbox(s, bd.MARGIN, Inches(4.95), bd.SLIDE_W - 2 * bd.MARGIN, Inches(1.6)) + bd._set_run(tf2.paragraphs[0].add_run(), + "An agent that gets sharper across sessions — it remembers, reconciles, " + "and answers better.", 20, "white", bold=True) + p2 = tf2.add_paragraph() + bd._set_run(p2.add_run(), + "Claude Managed Agents + Memory • Card D: Helios Sales Engineer • " + "Prospect: Northwind Bank", 13, "muted") + return s + + +def exec_what_why_slide(prs): + """Condensed 'what it is + why it's different' on one slide.""" + s = bd._blank_slide(prs) + bd._bg(s) + top = bd._heading(s, "Memory is not a vector database", kicker="What we built") + _, tf = bd._textbox(s, bd.MARGIN, top, bd.SLIDE_W - 2 * bd.MARGIN, Inches(1.0)) + bd._set_run(tf.paragraphs[0].add_run(), + "It means the agent decides what to remember, what to forget, and what to " + "update when it learns something new — across sessions, on the same account.", + 18, "ink", bold=True) + # three-up value tiles + tiles = [ + ("Remembers", "Durable facts about the prospect, every objection, and our best answer."), + ("Reconciles", "When new info contradicts the old, it updates in place — not blindly appends."), + ("Proves it", "An audit trail shows exactly what changed between calls."), + ] + col_w = (bd.SLIDE_W - 2 * bd.MARGIN - Inches(0.8)) / 3 + y = top + Inches(1.4) + ph = Inches(2.9) + for i, (h, body) in enumerate(tiles): + x = bd.MARGIN + i * (col_w + Inches(0.4)) + bd._panel(s, x, y, col_w, ph, "surface") + _, t = bd._textbox(s, x + Inches(0.25), y + Inches(0.25), col_w - Inches(0.5), ph - Inches(0.5)) + bd._set_run(t.paragraphs[0].add_run(), h, 20, "accent_dark", bold=True) + p = t.add_paragraph(); p.space_before = Pt(10) + bd._set_run(p.add_run(), body, 14, "muted") + return s + + +def exec_ask_slide(prs): + s = bd._blank_slide(prs) + bd._bg(s, "ink") + bd._accent_bar(s) + _, tf = bd._textbox(s, bd.MARGIN, Inches(0.7), bd.SLIDE_W - 2 * bd.MARGIN, Inches(0.5)) + bd._set_run(tf.paragraphs[0].add_run(), "WHY IT MATTERS", 14, "accent", bold=True, + font=C.BRAND["title_font"]) + _, tf2 = bd._textbox(s, bd.MARGIN, Inches(1.5), bd.SLIDE_W - 2 * bd.MARGIN, Inches(0.8)) + bd._set_run(tf2.paragraphs[0].add_run(), + "It answers the questions every enterprise buyer asks", 26, "white", bold=True, + font=C.BRAND["title_font"]) + y = Inches(2.7) + rh = Inches(0.78) + for q, a in C.BUSINESS_VALUE: + _, tf = bd._textbox(s, bd.MARGIN, y, bd.SLIDE_W - 2 * bd.MARGIN, rh) + r = tf.paragraphs[0].add_run(); bd._set_run(r, "• ", 15, "accent", bold=True) + bd._set_run(tf.paragraphs[0].add_run(), q + " ", 15, "white", bold=True) + bd._set_run(tf.paragraphs[0].add_run(), a, 14, "muted") + y += rh + return s + + +def build(): + prs = Presentation() + prs.slide_width = bd.SLIDE_W + prs.slide_height = bd.SLIDE_H + + exec_title_slide(prs) # 1 — title + exec_what_why_slide(prs) # 2 — what it is / why it's different + bd.comparison_slide(prs) # 3 — same question, sharper answer (the proof) + bd.memory_diff_slide(prs) # 4 — exactly what it learned (audit trail) + exec_ask_slide(prs) # 5 — why it matters / buyer questions + + os.makedirs("outputs", exist_ok=True) + prs.save(OUT_PATH) + return len(prs.slides._sldIdLst) + + +if __name__ == "__main__": + count = build() + print("Wrote %s (%d slides)" % (OUT_PATH, count)) diff --git a/create_agent.py b/create_agent.py index ddff15b..0afe920 100644 --- a/create_agent.py +++ b/create_agent.py @@ -8,76 +8,133 @@ reads and writes it with normal file tools. It persists across sessions — that's the whole point of this track. -IDs are saved to .agent_id, .environment_id, .memory_store_id so the -run_session_* scripts can pick them up. +The agent and environment are shared; the memory store is created PER CUSTOMER +so two prospects can be pitched with fully isolated memory (stretch goal S5). +Pass --customer to scope the store (default: northwind). + +IDs are saved to .agent_id, .environment_id, and .memory_store_ +(plus .memory_store_id for the most recently created store, which the +run_session_* scripts pick up) so the other scripts can find them. Usage: export ANTHROPIC_API_KEY="sk-ant-..." - python create_agent.py + python create_agent.py # customer = northwind + python create_agent.py --customer globex """ +import argparse import os from pathlib import Path from anthropic import Anthropic +try: + from dotenv import load_dotenv + + load_dotenv() # pick up ANTHROPIC_API_KEY from a local .env if present +except ImportError: + pass + + +DEFAULT_CUSTOMER = "northwind" + SYSTEM_PROMPT = """\ -You are the Institutional Memory Agent for a fast-growing company. +You are the Sales-Engineering Memory Agent supporting a sales engineer pitching +Helios — a real-time data-streaming platform — to a specific prospect. -Your job: be the smartest possible answer to questions about how this company -works — its policies, its people, its customers, its product. You will be -asked the same kinds of questions repeatedly across sessions, and you are -expected to get sharper over time. +Your job: be the sharpest possible partner going into the next call. Track the +prospect's environment, every objection they raise and our latest best answer, +competitive intelligence, and the current pitch strategy. You are asked about +the same deal repeatedly across sessions, and you are expected to get sharper +every call. # Memory protocol (mandatory) -You have a persistent memory store mounted at `/mnt/memory/`. It survives -across sessions. Treat it like the team wiki. +You have a persistent memory store mounted at `/mnt/memory/`. It survives across +sessions. Treat it like the deal's living account plan. -1. **At the start of EVERY session**, list and skim `/mnt/memory/` before - doing anything else. Use your bash and file tools. +1. **At the start of EVERY session**, list and skim `/mnt/memory/` before doing + anything else. Use your bash and file tools. 2. Read any files that look relevant to the current question. -3. As you work, **record what you learn for future sessions**: - - Policies (especially anything with a date or version) - - Key people in named roles - - Customer-specific facts - - Recurring questions and your best answer +3. As you work, **record what you learn for future sessions**. 4. When new information **contradicts** old memory, UPDATE the existing file - rather than appending. Note the effective date. Trust the newer version. -5. Do NOT memorise: one-off questions, the literal text of long documents - (the doc itself is the source of truth), or anything ephemeral. + rather than appending. Note the effective date. Trust the newer, dated + version (but see "Handling contradictions" below before overwriting). +5. Do NOT memorise one-off details or the verbatim text of long documents + (the document itself is the source of truth). + +# What to remember vs. not (be disciplined) + +ALWAYS remember: +- Prospect environment facts (stack, scale, constraints, compliance posture). +- Every objection + our best/latest answer to it. +- Competitive intelligence, each tagged with the date you learned it. +- Pitch-strategy decisions (sequence, what to lead with, what to de-emphasise). +- Key prospect contacts and their roles. + +NEVER remember: +- Verbatim deck text — the deck is the source of truth, not memory. +- One-off scheduling or logistics (call times, meeting links, room numbers). +- Unconfirmed or speculative pricing. +- Personal data beyond business contact details. + +# Handling contradictions (flag-and-ask) + +- A new document carrying a LATER effective date or a stated reason supersedes + memory: update the entry and trust the newer version, noting the date. +- BUT if a new document contradicts memory and carries **no effective date and + no stated reason**, do NOT silently overwrite. **FLAG the conflict and ASK** + which source to trust, showing both versions side by side. Only update once + the conflict is resolved. # How to answer -- If your answer relies on memory, lead with: "Based on what I learned in our - last session about X..." +- If your answer relies on memory, lead with: "Based on what I learned about + this deal last call about X..." - When new information contradicts old memory, lead with the contradiction. Don't paper over it. -- Be concise. +- Be concise and action-oriented — this is prep for a live call. """ def main() -> None: + parser = argparse.ArgumentParser( + description="Provision the Helios sales-engineer memory agent and a " + "per-customer memory store." + ) + parser.add_argument( + "--customer", + default=DEFAULT_CUSTOMER, + help="Prospect/customer id. Each customer gets its own isolated memory " + f"store, recorded in .memory_store_. Default: {DEFAULT_CUSTOMER}.", + ) + args = parser.parse_args() + customer = args.customer.strip().lower() + if not os.environ.get("ANTHROPIC_API_KEY"): raise SystemExit("Set ANTHROPIC_API_KEY before running.") client = Anthropic() - # 1. Agent + # 1. Agent (shared across customers) agent = client.beta.agents.create( - name="Institutional Memory Agent", + name="Helios Sales-Engineer Memory Agent", model="claude-sonnet-4-6", system=SYSTEM_PROMPT, tools=[{"type": "agent_toolset_20260401"}], - metadata={"hackathon": "partner-basecamp-2026", "track": "memory-agent"}, + metadata={ + "hackathon": "partner-basecamp-2026", + "track": "memory-agent", + "scenario": "card-d-sales-engineer", + }, ) Path(".agent_id").write_text(agent.id) print(f"Agent created: {agent.id}") - # 2. Environment (the cloud container) + # 2. Environment (the cloud container, shared across customers) environment = client.beta.environments.create( - name="memory-agent-env", + name="helios-se-env", config={ "type": "cloud", "networking": {"type": "unrestricted"}, @@ -86,18 +143,23 @@ def main() -> None: Path(".environment_id").write_text(environment.id) print(f"Environment created: {environment.id}") - # 3. Memory store — the thing that persists across sessions + # 3. Memory store — created per customer so two prospects never share + # memory (S5). The customer_id is baked into the store name/description + # so it's traceable in the Console and at session-attach time. memory_store = client.beta.memory_stores.create( - name="Institutional Memory", + name=f"Helios SE Memory — {customer}", description=( - "Persistent memory for the Institutional Memory Agent. Contains " - "policies, key people, customer facts, and recurring Q&A learned " - "across sessions. Used as authoritative wiki — newer entries " - "supersede older ones on the same topic." + f"Persistent sales-engineering memory for the Helios deal with " + f"'{customer}'. Contains the prospect's environment, objections + " + f"our latest answers, dated competitive intel, pitch strategy, and " + f"key contacts. Authoritative account plan — newer dated entries " + f"supersede older ones on the same topic. customer_id={customer}." ), ) + # Per-customer file (S5 isolation) + the default file the run scripts read. + Path(f".memory_store_{customer}").write_text(memory_store.id) Path(".memory_store_id").write_text(memory_store.id) - print(f"Memory store created: {memory_store.id}") + print(f"Memory store created: {memory_store.id} (customer={customer})") print("\nSetup complete.") print(f" Inspect the memory store in the Console at:") diff --git a/create_customer_store.py b/create_customer_store.py new file mode 100644 index 0000000..bc1f865 --- /dev/null +++ b/create_customer_store.py @@ -0,0 +1,74 @@ +""" +S5 — Per-tenant memory: create one memory store PER CUSTOMER. + +The agent and environment are persona-level and shared across all customers +(E1's create_agent.py creates them). Isolation lives at the *memory store* +level: each customer (prospect) gets its own store, so Northwind facts can +never leak into a Globex session and vice versa. + +This script is standalone — it does NOT touch create_agent.py. It reuses the +shared .agent_id / .environment_id and creates a fresh memory store named for +the customer, recording its id in `.memory_store_`. + +It also writes `.memory_store_id` = the store it just created, so the +unchanged inspect_memory.py targets whichever customer you last operated on. + +Usage: + python create_customer_store.py --customer northwind + python create_customer_store.py --customer globex +""" + +import argparse +import os +from pathlib import Path + +from anthropic import Anthropic + + +def main() -> None: + parser = argparse.ArgumentParser(description="Create a per-customer memory store.") + parser.add_argument( + "--customer", + default="northwind", + help="Customer / prospect id (default: northwind). One store per id.", + ) + args = parser.parse_args() + customer = args.customer.strip().lower() + + if not os.environ.get("ANTHROPIC_API_KEY"): + raise SystemExit("Set ANTHROPIC_API_KEY before running.") + + for required in (".agent_id", ".environment_id"): + if not Path(required).exists(): + raise SystemExit(f"Missing {required}. Run create_agent.py (E1) first.") + + client = Anthropic() + + store_path = Path(f".memory_store_{customer}") + if store_path.exists(): + store_id = store_path.read_text().strip() + print(f"Reusing existing memory store for '{customer}': {store_id}") + else: + memory_store = client.beta.memory_stores.create( + name=f"Helios SE Memory — {customer}", + description=( + f"Per-tenant Helios sales-engineering memory for customer_id=" + f"{customer}. Isolated from every other customer's store. " + "Contains this prospect's environment, objections + our latest " + "answers, competitive intel, and pitch strategy." + ), + ) + store_id = memory_store.id + store_path.write_text(store_id) + print(f"Memory store created for '{customer}': {store_id}") + + # Point the unchanged inspect_memory.py at the customer we just touched. + Path(".memory_store_id").write_text(store_id) + + print(f" Recorded in {store_path}") + print(f" .memory_store_id now points at '{customer}' (for inspect_memory.py)") + print(f"\nNext: python run_customer_session.py --customer {customer}") + + +if __name__ == "__main__": + main() diff --git a/deck_content.py b/deck_content.py new file mode 100644 index 0000000..b5f1a5d --- /dev/null +++ b/deck_content.py @@ -0,0 +1,247 @@ +"""Illustrative *expected output* for the Card-D (Helios Sales Engineer) demo. + +Card D has not been run live yet, so the session answers and memory snapshots +below are illustrative — grounded in the behaviour described in +``execution-plan-card-d.md`` (section 1a) and ``stretch-goals.md``. The deck +(``build_deck.py``) reads from this module; rendering is kept separate from +content so this file can later be replaced with parsed real output +(``outputs/session1.txt`` / ``session2.txt`` / memory-snapshot JSON) without +touching the layout code. +""" + +# --- Brand / styling ------------------------------------------------------- +# Colours are (R, G, B) tuples; build_deck.py converts them to pptx RGBColor. +BRAND = { + "ink": (23, 23, 33), # near-black body text + "muted": (107, 114, 128), # secondary text + "accent": (217, 119, 6), # Helios amber — titles / accents + "accent_dark": (146, 64, 14), + "session1": (37, 99, 235), # blue — "before" + "session2": (5, 150, 105), # green — "after / sharper" + "added": (5, 150, 105), # green — memory added + "changed": (217, 119, 6), # amber — memory changed + "unchanged": (107, 114, 128), # grey — memory unchanged + "surface": (245, 245, 247), # light panel background + "white": (255, 255, 255), + "title_font": "Calibri", + "body_font": "Calibri", +} + +TEST_QUESTION = "Tomorrow's call is the final pitch. What's our strategy?" + +SCENARIO = { + "product": "Helios — a real-time data-streaming platform", + "prospect": "Northwind Bank (fintech; SOC2 / PCI; EU + US data residency)", + "persona": ( + "An agent supporting a sales engineer pitching Helios. It tracks the " + "prospect's environment, every objection + our latest best answer, " + "competitive intel, and the current pitch strategy — and gets sharper " + "each call." + ), + "round1_docs": [ + ("helios-stack-overview.md", + "Northwind's environment: self-managed Kafka on-prem, AWS for " + "analytics, EU+US residency, SOC2/PCI, ~2M events/sec peak, Flink, " + "6-person platform team."), + ("objections-log.md", + "Five prior objections + our current best answer each: latency SLA, " + "data residency, total cost vs Kafka, migration risk, vendor lock-in."), + ("pitch-deck.md", + "Current sequence: problem -> architecture -> managed-vs-self TCO -> " + "security/residency -> migration path -> pricing -> next steps."), + ], + "round2_docs": [ + ("new-objection-2026-05-22.md", + "Northwind's new CISO mandates active-active multi-region failover " + "with <5s RPO — the current pitch does not address it."), + ("competitive-update-2026-05-25.md", + "StreamCorp cut prices ~20% and shipped a managed connector Helios " + "lacks — but Helios just shipped native active-active multi-region " + "(answers the new objection) and residency pinning."), + ], +} + +# --- Session answers (illustrative) --------------------------------------- +SESSION_1_ANSWER = { + "preface": "Memory store is empty — this is a fresh start. Answering from the round-1 docs.", + "headline": "Run the standard deck in its current order.", + "bullets": [ + "Open on the problem, then Helios architecture.", + "Lead the value case with managed-vs-self-managed TCO vs their Kafka.", + "Cover security & EU/US data residency (SOC2/PCI).", + "Walk the migration path to de-risk the switch from self-managed Kafka.", + "Close on pricing, then next steps.", + "Have answers ready for the five known objections: latency SLA, " + "residency, cost, migration risk, lock-in.", + ], + "gap": "No failover / multi-region story — it has not come up yet.", +} + +SESSION_2_ANSWER = { + "preface": ( + "Memory recalls the round-1 strategy and the five objections. Two new " + "round-2 inputs change the plan — reconciling, then resequencing." + ), + "headline": "Resequence: open on resilience, reframe on value, pre-empt the new objection.", + "bullets": [ + "OPEN on active-active multi-region failover with <5s RPO — Helios just " + "shipped it natively; this directly answers the CISO's new mandate.", + "Pair it with residency pinning — closes the EU/US residency objection " + "in the same breath.", + "Pre-empt the new CISO objection rather than waiting for it to be raised.", + "Reframe pricing against StreamCorp's ~20% cut: lead with TCO + native " + "failover (no bolt-on), don't compete on sticker price.", + "Get ahead of StreamCorp's managed connector gap — name it, give the " + "roadmap/workaround before they do.", + "Then run architecture -> TCO -> migration -> next steps as before.", + ], + "gap": "Resilience + competitive reframing moved to the front; pricing demoted.", +} + +# --- Memory snapshots (path-organized, illustrative) ----------------------- +# Each entry: path -> short content summary the agent chose to remember. +MEMORY_SNAPSHOT_AFTER_S1 = { + "/customers/northwind/environment.md": + "Self-managed Kafka on-prem; AWS analytics; EU+US residency; SOC2/PCI; " + "~2M events/sec; Flink; 6-person platform team.", + "/objections/latency.md": "Latency SLA vs self-managed Kafka — best answer: .", + "/objections/residency.md": "Data residency / on-prem requirement — best answer: .", + "/objections/cost.md": "Total cost vs existing Kafka — best answer: .", + "/objections/migration.md": "Migration risk — best answer: phased cutover.", + "/objections/lock-in.md": "Vendor lock-in — best answer: open protocols.", + "/pitch/strategy.md": + "Sequence: problem -> architecture -> TCO -> security/residency -> " + "migration -> pricing -> next steps.", +} + +MEMORY_SNAPSHOT_AFTER_S2 = { + "/customers/northwind/environment.md": + "Self-managed Kafka on-prem; AWS analytics; EU+US residency; SOC2/PCI; " + "~2M events/sec; Flink; 6-person platform team.", + "/customers/northwind/contacts.md": + "New CISO (joined ~May 2026) — mandates active-active multi-region, <5s RPO.", + "/objections/latency.md": "Latency SLA vs self-managed Kafka — best answer: .", + "/objections/residency.md": + "Data residency — UPDATED: now answered by Helios residency pinning.", + "/objections/cost.md": + "Total cost — UPDATED: reframe vs StreamCorp -20%; lead on TCO + native failover.", + "/objections/migration.md": "Migration risk — best answer: phased cutover.", + "/objections/lock-in.md": "Vendor lock-in — best answer: open protocols.", + "/objections/failover.md": + "NEW objection: active-active multi-region failover, <5s RPO " + "(CISO mandate) — answered by Helios native active-active.", + "/competitive/streamcorp.md": + "StreamCorp cut prices ~20%; shipped a managed connector Helios lacks. " + "Counter: native active-active + residency pinning; reframe on TCO.", + "/pitch/strategy.md": + "RESEQUENCED: open on failover + residency -> reframe pricing vs " + "StreamCorp -> architecture -> TCO -> migration -> next steps.", +} + + +def _compute_diff(before, after): + """Return (added, changed, unchanged) path lists between two snapshots.""" + added = [p for p in after if p not in before] + changed = [p for p in after if p in before and after[p] != before[p]] + unchanged = [p for p in after if p in before and after[p] == before[p]] + return added, changed, unchanged + + +_added, _changed, _unchanged = _compute_diff( + MEMORY_SNAPSHOT_AFTER_S1, MEMORY_SNAPSHOT_AFTER_S2 +) + +MEMORY_DIFF = { + "added": [(p, MEMORY_SNAPSHOT_AFTER_S2[p]) for p in _added], + "changed": [(p, MEMORY_SNAPSHOT_AFTER_S2[p]) for p in _changed], + "unchanged": [p for p in _unchanged], +} + +# Facts-per-category for the memory-growth bar chart: (category, after_s1, after_s2) +MEMORY_GROWTH = [ + ("Environment", 1, 2), + ("Objections", 5, 6), + ("Competitive", 0, 1), + ("Pitch strategy", 1, 1), +] + +# --- Stretch goals S1-S9 --------------------------------------------------- +# (id, title, what, why_it_lands) +STRETCH_TIERS = [ + ("Tier 1 — Make memory deliberate", [ + ("S1", "Explicit memory policy", + "ALWAYS/NEVER lists in the system prompt (remember environment, " + "objections+answers, dated competitive intel, pitch decisions; never " + "verbatim deck text or speculative pricing).", + "'What does my agent remember?' is the first question every " + "privacy/security team asks."), + ("S2", "Memory Curator sub-agent", + "A second agent does housekeeping on the main store — merges " + "superseded objection answers, keeps only the latest intel per competitor.", + "Memory hygiene as a role, not a feature — maps to how human teams " + "keep institutional knowledge clean."), + ]), + ("Tier 2 — Stress-test memory", [ + ("S3", "Adversarial round", + "Feed undated docs that contradict memory with no reason. Correct " + "behaviour: flag the conflict and ask which to trust — never silently " + "overwrite.", + "Shows memory can be engineered to resist bad inputs — the only way " + "clients will trust it."), + ("S4", "'What have you learned?'", + "A session with no new docs; one prompt asks the agent to summarise " + "everything it has learned across calls.", + "The memory store talking back — the most direct, demo-able proof of " + "what's retained."), + ]), + ("Tier 3 — Make memory production-shaped", [ + ("S5", "Per-tenant memory", + "One memory store per customer_id (Northwind vs Globex). Prove " + "Northwind facts never leak into Globex answers.", + "First question every multi-tenant SaaS asks: how do you separate " + "customers' data?"), + ("S6", "Scheduled ingestion", + "A watcher ingests new docs from an inbox folder on a schedule and " + "moves them to processed — memory accumulates passively.", + "A fully autonomous, continuously-learning agent."), + ("S7", "Long-context + compaction", + "20+ turns in one session; rely on built-in compaction, then commit a " + "compacted call summary to the memory store.", + "Distinguishes in-session memory (context window) from across-session " + "memory (the store) — a gap most teams haven't internalised."), + ]), + ("Tier 4 — For the showoffs", [ + ("S8", "Memory diff view", + "Snapshot the store after each session and diff them — print exactly " + "what was added / changed / removed between calls.", + "This is the demo headline: 'here's precisely what the agent learned.'"), + ("S9", "Topic sub-agents", + "Group memory into topics, spawn one expert sub-agent per topic seeded " + "with its slice, route the question to the right expert(s) and synthesise.", + "An org chart built out of memory — the architecture in every " + "'AI-native enterprise' pitch six months out."), + ]), +] + +BUSINESS_VALUE = [ + ("\"What does it remember?\"", + "An explicit ALWAYS/NEVER policy (S1) — answer it precisely, on demand."), + ("\"How is it kept clean?\"", + "A curator sub-agent (S2) merges superseded facts and prunes stale intel."), + ("\"How do you isolate customers?\"", + "Per-tenant memory stores by customer_id (S5) — provable no-leak."), + ("\"Can I see what changed?\"", + "Memory diff between sessions (S8) — an audit trail of what was learned."), + ("\"Won't it just trust bad data?\"", + "Adversarial flag-and-ask behaviour (S3) — contradictions surface, not silently overwrite."), +] + +HOW_TO_RUN = [ + "pip install -r requirements.txt (now includes python-pptx)", + "python create_agent.py — Managed Agent + path-organized memory store", + "python run_session_1.py — round-1 docs; baseline answer -> outputs/session1.txt", + "python inspect_memory.py — show what the agent chose to remember", + "python run_session_2.py — round-2 docs; sharper answer -> outputs/session2.txt", + "diff the two outputs — session 2 anticipates failover, cites StreamCorp, resequences", + "python build_deck.py — regenerate this deck from deck_content.py", +] diff --git a/diff.html b/diff.html new file mode 100644 index 0000000..e04e287 --- /dev/null +++ b/diff.html @@ -0,0 +1,764 @@ + + + + + + REKALL — Memory Diff + + + + + +
+ +
Memory Implant Systems  ·  Diff Report  ·  2026-06-02
+
+ SAME QUESTION.  TWO SESSIONS.  THE AGENT REMEMBERED. +
+
+
+ Session 1 + Round 1 docs only · No prior memory +
+
+ ⟵ Memory stored ⟶ + northwind-bank-presales.md +
+
+ Session 2 + Memory + Round 2 docs · Conflicts reconciled +
+
+
+ +
+ + + +
+ +
+
NEW INTELLIGENCE
+

Sven Aaltonen — New Blocker

+

New CISO not in Session 1 at all. Session 2 leads with his board mandate: active-active failover, sub-5s RPO. Helios shipped this GA on 2026-05-25. StreamCorp cannot match it.

+
+ +
+
NEW INTELLIGENCE
+

StreamCorp Price Cut

+

StreamCorp dropped to $0.88/GB (was $1.10). Session 2 reframes this as a TCO argument in Helios's favour: their price excludes active-active, which wipes out the advantage.

+
+ +
+
STRUCTURAL CHANGE
+

Deck Resequenced

+

Session 1: open with Hina's pain, TCO on slide 6, pricing on slide 11.
+ Session 2: resilience slide moves before pricing — Sven's objection must be answered before the room hears a number.

+
+ +
+
UPGRADED ANSWER
+

Residency Now GA

+

Session 1 relied on an attestation in review. Session 2: broker-level record pinning is now GA — hardware enforced, customer-controlled audit topic. Marcus's objection moves from open to closed.

+
+ +
+
NEW CLOSE ELEMENT
+

Named TAM Offer

+

Not in Session 1. Session 2 adds: offer named TAM + weekly check-ins for 90 days post-cutover as a concession to Sven. (Requires internal delivery sign-off first.)

+
+ +
+
DEPRIORITISED
+

Owen Mbeki + Hina Park

+

Session 1 gave both a named win condition. Session 2 drops Owen entirely and reduces Hina to a cheat-sheet line. The new critical path runs through Sven → Klara → Marcus → Raghav.

+
+ +
+ + + + +
+
+
SESSION 1
+
+
01
Open: Hina's pain quote ("Every weekend on-call is at least one Kafka page")
+
02
Stakeholder win conditions (Klara, Raghav, Marcus, Hina, Owen)
+
03
TCO — the hinge (slide 6). Flink savings + incident hours flip the 28% premium
+
04
Marcus's residency slide (attestation in review, don't oversell)
+
05
Migration runbook (Raghav's slide — physical appendix)
+
06
Pricing / lock-in (slide 11 — CPI-capped term, legal review in progress)
+
07
Close: bake-off cluster · residency attestation · TCO model review
+
+
+
+
SESSION 2
+
+
01
Open: Active-active resilience — Sven's board mandate. GA 2026-05-25. StreamCorp can't match.
+
02
Acknowledge StreamCorp price cut proactively — reframe as TCO argument
+
03
TCO — Helios active-active is included in Enterprise. StreamCorp's price excludes it.
+
04
Residency pinning now GA — broker-level, hardware enforced. Marcus: open → closed.
+
05
Migration runbook v0.4 + bake-off cluster (Raghav)
+
06
Pricing — hold list price. Discount only via standard approval flow.
+
07
Close: Named TAM + 90-day check-ins (Sven) · runbook final review · bake-off cluster · deck v3.2
+
+
+
+ + + + +
+ +
+
SESSION 1
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PersonWin condition
Klara Voss
Economic buyer
Break-even at month 22 holds up against her numbers
Raghav Iyer
Streaming Tech Lead
Bake-off cluster + credible migration runbook
Marcus Lehmann
Security Architect
Clear path to residency attestation sign-off by end of Q1
Hina Park
SRE Lead
Tangible picture of post-migration on-call burden
Owen Mbeki
Sr. Platform Engineer
Open-source concern already closed — keep warm
— CISO not in room / not mentioned —
+
+ +
+
SESSION 2
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PersonTheir answer
Sven Aaltonen
CISO — NEW
Active-active GA · Frankfurt↔Dublin 12ms · tested 1.2M/sec · StreamCorp can't match
Klara Voss
Economic buyer
StreamCorp's $0.88/GB excludes active-active. Full TCO favours Helios.
Marcus Lehmann
Security Architect
Broker-level pinning GA + signed attestation = CLOSED
Raghav Iyer
Streaming Tech Lead
Bake-off cluster · runbook v0.4 · topic-by-topic cutover
Hina Park
SRE Lead
Automatic failover → no 3am pager for region events
Owen Mbeki
Sr. Platform Engineer
Not mentioned — deprioritised
+
+
+ + + + +
+

// /mnt/memory/institutional-memory/

+
+
northwind-bank-presales.md  ↑ UPDATED
+
gaps-and-open-questions.md  (stale)
+
+
+ northwind-bank-presales.md now contains: + Sven's active-active mandate · Helios GA 2026-05-25 · StreamCorp price cut details · + broker-level residency pinning · updated stakeholder map · deck v3.2 sequence · + open internal actions (TAM sign-off, runbook review, bake-off cluster) +
+
+ + + +
+
MEMORY RECONCILIATION: COMPLETE
+
+ Session 1 answered from documents alone — no prior context. + The agent saved the strategy to memory. + Session 2 read that memory first, encountered two contradictions + (Sven's new blocker and StreamCorp's price cut), + and updated its position — it didn't append, it reconciled. + The result: a materially sharper answer that anticipated the hardest objection in the room + before it was raised. +
+
+ +
+ +
+ REKALL INC.  ·  WE CAN REMEMBER IT FOR YOU WHOLESALE  ·  TOTAL-RECALL v2.0 +
+ + + diff --git a/execution-plan-card-d.md b/execution-plan-card-d.md new file mode 100644 index 0000000..df98f89 --- /dev/null +++ b/execution-plan-card-d.md @@ -0,0 +1,142 @@ +# Execution Plan — Card D (Sales Engineer for Product X) + All Stretch Goals + +## Context + +This repo (`institutional-memory-main`) is a **Claude Managed Agents + Memory tool** demo: an agent runs two sessions on the same domain, memory persists between them, session-2 docs contradict session-1, and the agent should reconcile and answer better. The scaffolding (`create_agent.py`, `run_session_1.py`, `run_session_2.py`, `inspect_memory.py`, `stretch_memory_curator.py`) is currently shaped for **Card A — New-Hire Onboarding**, and `synthetic-data/round1`+`round2` hold onboarding docs. + +We are switching to **Card D — Sales Engineer for Product X** and **replacing** the onboarding scenario in place. Product X = **Helios**, a fictional real-time data-streaming platform; the prospect being pitched is **Northwind Bank** (fintech). Goal: a working Card-D demo plus **all nine stretch goals (S1–S9)**. + +**Card D specifics (from `scenario-cards.md`):** +- Persona: agent supporting a sales engineer pitching Helios; it learns the customer's environment, their objections, and our latest answers. +- Round 1 docs: customer stack overview, list of objections from previous calls, current pitch deck. +- Round 2 docs: a new objection from the latest call, a competitive update from product management. +- Test question (both sessions): **"Tomorrow's call is the final pitch. What's our strategy?"** +- "Better in session 2" = anticipates the new objection, references the latest competitive update, and adjusts the pitch *sequence* rather than repeating the original plan. + +**API facts grounded during planning:** +- Managed Agents = `client.beta.agents.create` / `environments.create` / `sessions.create` / `sessions.events.stream`/`send`; memory via `client.beta.memory_stores.create` + `memories.list(store_id, path_prefix=...)` + `memories.retrieve(id, memory_store_id=...)`. Memory is **path-organized** inside a store. +- Compaction is built into the Managed Agents harness; the Messages-API form is `context_management={"edits":[{"type":"compact_20260112"}]}` with beta header `compact-2026-01-12`. +- There is **no documented standalone "Routines" API** (the routines doc 404s). S6 therefore uses a scheduled-wrapper approach (OS scheduler), with a note to prefer a native routines API if one exists in the SDK at execution time. + +--- + +## Part 1 — Core Build (Card D) + +### 1a. Replace synthetic data +**Overwrite** `synthetic-data/round1/` (3 docs) and `synthetic-data/round2/` (2 docs). Match the existing markdown style (dated headers, "effective" dates, tables) so contradictions are detectable by date. + +`synthetic-data/round1/`: +- `helios-stack-overview.md` — Northwind Bank's current environment: Kafka self-managed on-prem, AWS for analytics, strict data-residency (EU + US), SOC2/PCI, peak ~2M events/sec, Flink for stream processing, a 6-person platform team. +- `objections-log.md` — objections raised in prior calls, each with *our current best answer*: (1) latency SLA vs self-managed Kafka, (2) data residency / on-prem requirement, (3) total cost vs existing Kafka, (4) migration risk, (5) vendor lock-in. +- `pitch-deck.md` — current pitch sequence: problem → Helios architecture → managed-vs-self-managed TCO → security/residency → migration path → pricing → next steps. + +`synthetic-data/round2/`: +- `new-objection-2026-05-22.md` — newest call surfaced a **new** objection: Northwind's new CISO mandates *active-active multi-region failover with <5s RPO*, which the current pitch doesn't address. +- `competitive-update-2026-05-25.md` — product management update: competitor **StreamCorp** just cut prices ~20% and announced a managed connector Helios lacks; **but** Helios shipped native active-active multi-region (directly answers the new objection) and a residency-pinning feature. + +### 1b. Repoint the agent + sessions +- **`create_agent.py`** — rewrite `SYSTEM_PROMPT` to the Helios Sales-Engineer persona: "You support a sales engineer pitching Helios to a specific prospect. Track the prospect's environment, every objection + our latest best answer, competitive intel, and the current pitch strategy. Get sharper each call." Keep the existing memory-protocol section. Update `name=` and `metadata` to the SE scenario. (Also receives S1 + S3 additions below.) +- **`run_session_1.py`** — set `TEST_QUESTION` to the Card-D question; reword `user_message` from "onboarding and policy documents" to "the prospect's stack overview, objection log, and current pitch deck." `DOCS_DIR` stays `round1`. +- **`run_session_2.py`** — same `TEST_QUESTION`; reword `user_message` to "a new objection from the latest call and a competitive update from product." `DOCS_DIR` stays `round2`. +- `inspect_memory.py` needs no change (store-agnostic). + +### 1c. Run + compare +`python create_agent.py` → `run_session_1.py` → `inspect_memory.py` → `run_session_2.py`. Diff `outputs/session1.txt` vs `outputs/session2.txt`: session 2 should anticipate the active-active objection, cite the StreamCorp price cut + Helios's new capability, and resequence the pitch. + +--- + +## Part 2 — Stretch Goals (all nine) + +### S1 — Explicit memory policy *(edit `create_agent.py`)* +Add an **ALWAYS remember** list (prospect environment facts; each objection + our best/latest answer; competitive intel with dates; pitch-strategy decisions; key prospect contacts & roles) and a **NEVER remember** list (verbatim deck text — the deck is the source of truth; one-off scheduling/logistics; unconfirmed/speculative pricing; personal data beyond business contact). Recreate the agent and re-run both sessions; memory store should be tighter. + +### S2 — Memory Curator sub-agent *(reuse `stretch_memory_curator.py`)* +Already implemented and largely persona-agnostic. Light tweak to `CURATOR_SYSTEM_PROMPT`: add "merge superseded objection answers; keep only the latest competitive intel per competitor." Run `python stretch_memory_curator.py` after session 2, then re-run session 2 to show a sharper answer. + +### S3 — Adversarial round *(new `synthetic-data/round3/` + `run_session_3.py`)* +Create `round3/` docs that contradict round1 **with no plausible reason / no effective date** (e.g., `bad-stack-overview.md` claiming Northwind is 100% on-prem with no AWS and only 50k events/sec, and a doc claiming the latency objection "was never raised"). Add `run_session_3.py` (copy of `run_session_2.py`, pointed at `round3`, prompt = "reconcile these against memory"). **Expected correct behaviour: flag + ask, not silent overwrite.** If it silently overwrites, harden `create_agent.py`: add a rule — "If a new document contradicts memory but carries no effective date or stated reason, FLAG the conflict and ASK which to trust; do not overwrite." Re-create + re-test. + +### S4 — "What have you learned?" session *(new `run_session_learned.py`)* +Copy session scaffolding but attach **no new docs**; single user message: *"Summarise everything you've learned about this domain across our previous sessions."* Save to `outputs/session_learned.txt`. This is the memory store "talking back." + +### S5 — Per-tenant memory via `customer_id` *(modify create + session scripts)* +Run two prospects — **Northwind Bank** and a second prospect **Globex Retail** — with isolated memory. Approach (recommended for guaranteed no-leak): **one memory store per `customer_id`**, recorded in `.memory_store_` files, with `customer_id` also set in `sessions.create(... metadata=...)` and in the store name/description. Add a `--customer` arg to `create_agent.py` (store creation) and the run scripts; load a small per-customer doc set. Run two sessions each for Northwind and Globex, then verify (via `inspect_memory.py` per store + a cross-question) that Northwind facts never appear in Globex answers. *(Lighter alternative if a single store is required: scope by path prefix `/customers//…` and pass `path_prefix` to `memories.list`.)* + +### S6 — Memory + scheduled ingestion ("Routines") *(new `ingest_new_docs.py` + `synthetic-data/inbox/`)* +No native Routines API is documented. Build `ingest_new_docs.py`: scans `synthetic-data/inbox/` for unseen `.md` files, runs a short session that ingests each into memory, then moves processed files to `synthetic-data/processed/`. Schedule it externally — document the **Windows Task Scheduler** command (and an equivalent cron line) to fire daily. Memory then accumulates passively. Note in code/comments: if `client.beta.routines.*` exists in the installed SDK at run time, prefer wrapping the session in that instead. + +### S7 — Long-context + compaction *(new `run_session_longcontext.py`)* +Drive **20+ turns in a single session** (a simulated long technical Q&A with Northwind: latency, residency, failover, connectors, pricing, migration, security review…), sending each turn via `sessions.events.send` and streaming the reply. Rely on the harness's built-in compaction for in-session sharpness; **final turn instructs the agent to write a compacted summary of the whole call into `/mnt/memory/`**. Comment the distinction: in-session memory (context window + compaction) vs across-session memory (the memory store). If demonstrating the Messages-API form too, reference `context_management={"edits":[{"type":"compact_20260112"}]}` + beta `compact-2026-01-12`. + +### S8 — Memory diff view *(new `memory_snapshot.py` + `memory_diff.py`)* +`memory_snapshot.py