<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Spandana S R</title>
    <description>The latest articles on DEV Community by Spandana S R (@spandysr).</description>
    <link>https://dev.to/spandysr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3838163%2Fdada1b43-92da-4255-bc4e-361bb235ea73.png</url>
      <title>DEV Community: Spandana S R</title>
      <link>https://dev.to/spandysr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/spandysr"/>
    <language>en</language>
    <item>
      <title>My agent remembered a rejected application and adjusted strategy</title>
      <dc:creator>Spandana S R</dc:creator>
      <pubDate>Sun, 22 Mar 2026 09:52:38 +0000</pubDate>
      <link>https://dev.to/spandysr/my-agent-remembered-a-rejected-application-and-adjusted-strategy-12g2</link>
      <guid>https://dev.to/spandysr/my-agent-remembered-a-rejected-application-and-adjusted-strategy-12g2</guid>
      <description>&lt;h1&gt;
  
  
  My agent remembered a rejected application and adjusted strategy
&lt;/h1&gt;

&lt;p&gt;"Wait — it already knows this company rejected you." I hadn't told the agent that. It had pulled the rejection from a previous session, cross-referenced the student's updated skill set, and quietly suggested a different role at the same firm.&lt;/p&gt;

&lt;p&gt;That moment is when I realised we'd built something genuinely different from the usual career chatbot. Not smarter — just less amnesiac.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;The project is a React + Vite chat app that acts as an AI career advisor for students. A student tells it what skills they have, what they've built, and where they've applied. The advisor gives resume feedback, flags skill gaps, and recommends internships.&lt;/p&gt;

&lt;p&gt;The hard part is that internship season plays out over weeks. A student talks to the advisor on Monday, gets rejected Thursday, learns a new framework over the weekend, and comes back the following Monday. A stateless agent treats that second conversation as a blank slate. Ours doesn't.&lt;/p&gt;

&lt;p&gt;The stack is deliberately lean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React + Vite&lt;/strong&gt; for the frontend — fast dev loop, clean env variable management&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groq&lt;/strong&gt; (&lt;code&gt;llama-3.3-70b-versatile&lt;/code&gt;) as the LLM — fast inference, generous free tier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/vectorize-io/hindsight" rel="noopener noreferrer"&gt;Hindsight&lt;/a&gt;&lt;/strong&gt; for persistent cloud memory, keyed per student via &lt;code&gt;VITE_HINDSIGHT_MEMORY_BANK&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;localStorage&lt;/strong&gt; as a zero-cost fallback when Hindsight keys aren't set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the interesting logic lives in one file: &lt;code&gt;src/services/advisorService.js&lt;/code&gt;. The rest of the app — &lt;code&gt;App.jsx&lt;/code&gt;, &lt;code&gt;ChatWindow&lt;/code&gt;, &lt;code&gt;ProfilePanel&lt;/code&gt; — is mostly UI plumbing around &lt;code&gt;sendMessage()&lt;/code&gt; and &lt;code&gt;getProfile()&lt;/code&gt;. The full source is on &lt;a href="https://github.com/Jagan83411/career-advisor" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The memory architecture
&lt;/h2&gt;

&lt;p&gt;Every time a student sends a message, &lt;code&gt;sendMessage()&lt;/code&gt; in &lt;code&gt;advisorService.js&lt;/code&gt; does three things in sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Recall&lt;/strong&gt; relevant memories from &lt;a href="https://hindsight.vectorize.io/" rel="noopener noreferrer"&gt;Hindsight&lt;/a&gt; using the student's message as the search query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call Groq&lt;/strong&gt; with those memories prepended to the system prompt&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract and save&lt;/strong&gt; any new career facts from the exchange back to &lt;a href="https://vectorize.io/features/agent-memory" rel="noopener noreferrer"&gt;Hindsight's agent memory&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's what Hindsight's graph view looks like after a real student session — three memory nodes connected by semantic and temporal links, tracking identity, skills, and an application in one graph:&lt;/p&gt;

&lt;p&gt;![Hindsight memory graph showing 3 nodes: student identity, skills in C/C++/Java, and Microsoft internship application]&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0ajrjw4y65tqzpjkefe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh0ajrjw4y65tqzpjkefe.png" alt=" " width="800" height="323"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The recall step is what makes the agent feel like it remembers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From sendMessage() in advisorService.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;memories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;memCtx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;memories&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`\nSTUDENT PROFILE FROM MEMORY:\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;memories&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;No previous profile found for this student.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Memory gets injected at the top of the system prompt&lt;/span&gt;
&lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;memCtx&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The memory block sits at the top of the system prompt, before the behavioral instructions. We tried appending it at the end first — the agent acknowledged the memories but underweighted them. Moving it to the top made the agent treat recalled facts as ground truth rather than a footnote.&lt;/p&gt;

&lt;h2&gt;
  
  
  How extraction works
&lt;/h2&gt;

&lt;p&gt;After every exchange, &lt;code&gt;extractAndSave()&lt;/code&gt; runs a second Groq call specifically to pull structured career facts out of what just happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// From extractAndSave() in advisorService.js&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Extract student career info from the conversation.
Return ONLY valid JSON with these fields (omit if not found):
{
  "skills": ["list of technical skills mentioned"],
  "projects": ["list of projects mentioned"],
  "applications": [{"company":"","role":"","status":"applied/interviewing/rejected/offered"}],
  "targetRoles": ["roles the student wants"]
}`&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Student said: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userMsg&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"\nAdvisor replied: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;aiReply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;..."`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The extracted JSON gets serialised into natural language before being saved to Hindsight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;memoryText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
Student ID: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
Skills: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;extracted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;skills&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
Projects: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;extracted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;projects&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
Applications: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;extracted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applications&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;company&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) - &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
Target Roles: &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;extracted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetRoles&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
`&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;saveMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;memoryText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The natural language format is intentional. Hindsight does its own fact extraction on ingestion — giving it structured prose lets its embedding model do semantic matching properly at recall time. Storing raw JSON would work for retrieval but makes the recalled context harder for the LLM to read when it lands in the system prompt.&lt;/p&gt;

&lt;p&gt;Here's the Hindsight table view after one real session with a student named Spandana — three clean memories stored under the &lt;code&gt;career_profile&lt;/code&gt; context, each tagged with the relevant entities:&lt;/p&gt;

&lt;p&gt;![Hindsight table view showing 3 stored memories: Microsoft internship application, C/C++/Java skills, and student identity for student_spandana]&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8zpmqyl9yenqtiryk69.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo8zpmqyl9yenqtiryk69.png" alt=" " width="800" height="157"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application status field — &lt;code&gt;applied / interviewing / rejected / offered&lt;/code&gt; — is the piece that made the rejection-memory scenario work. When a student says "Goldman passed on me," the extractor tags it as &lt;code&gt;status: "rejected"&lt;/code&gt;. Next session, that fact gets recalled when the student asks where to apply, and the agent skips Goldman without being told again.&lt;/p&gt;
&lt;h2&gt;
  
  
  The localStorage fallback
&lt;/h2&gt;

&lt;p&gt;One decision I'm glad we made early: the app degrades gracefully when Hindsight keys aren't set. &lt;code&gt;shouldUseHindsight()&lt;/code&gt; checks at runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shouldUseHindsight&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HINDSIGHT_KEY&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;MEMORY_BANK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every memory operation — &lt;code&gt;saveMemory()&lt;/code&gt; and &lt;code&gt;getMemory()&lt;/code&gt; — calls this first. If Hindsight is unavailable or throws, the code falls back to a local store in &lt;code&gt;localStorage&lt;/code&gt;, keyed by &lt;code&gt;userId&lt;/code&gt;. The local store even does lightweight keyword scoring to approximate semantic recall:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getLocalMemory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;scored&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's token overlap, not embeddings — but for a career advisor where queries tend to be domain-specific ("backend role", "Goldman rejection", "Python skills"), it's surprisingly effective locally. The store caps at 80 entries per user so localStorage doesn't grow unbounded.&lt;/p&gt;

&lt;p&gt;This meant the whole team could develop and demo without burning Hindsight credits. Only when we needed memory to persist across devices did we flip on the cloud keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;Here's the same scenario before and after adding memory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Without memory:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Student: I applied to Goldman last week, no response yet. Where else should I apply?&lt;/p&gt;

&lt;p&gt;Agent: Based on your Python background, you might consider Goldman Sachs, Two Sigma, or Citadel for quant roles...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent recommended Goldman — the company the student had just told it they'd applied to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With Hindsight memory:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Student: I applied to Goldman last week, no response yet. Where else should I apply?&lt;/p&gt;

&lt;p&gt;Agent: Since Goldman is already in play, I'll skip that one. Based on the ML project you mentioned last session and your Python + scikit-learn skills, Stripe's data engineering internship looks like a strong fit — their stack maps closely to what you've built...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same model. Same system prompt. The only difference was the recalled context injected before the Groq call. The agent knew about the Goldman application because &lt;code&gt;extractAndSave()&lt;/code&gt; had tagged it with &lt;code&gt;status: "applied"&lt;/code&gt; in a previous session and &lt;code&gt;getMemory()&lt;/code&gt; had surfaced it when the student asked where to apply.&lt;/p&gt;

&lt;p&gt;The rejection case was more striking. When a student came back after being turned down, the recalled memory included the rejection status. If their skills had grown since — say they'd added a new project the extractor had saved — the agent would suggest a different role at the same company. No logic change. Just better input.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we got wrong
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The extraction prompt needed application status from the start.&lt;/strong&gt; Our first version stored skills and projects but treated all applications the same. We couldn't distinguish a rejection from a pending application. Adding the &lt;code&gt;status&lt;/code&gt; enum (&lt;code&gt;applied / interviewing / rejected / offered&lt;/code&gt;) to the extraction prompt was a one-line change that made the rejection-memory behaviour possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;getProfile()&lt;/code&gt; is still keyword matching, not proper recall.&lt;/strong&gt; The profile panel in the UI calls &lt;code&gt;getProfile()&lt;/code&gt;, which does string matching over recalled memories (&lt;code&gt;if text includes "python"&lt;/code&gt;) rather than anything structured. It works for the demo but would break on any edge case — "I don't know Python" would still add Python to the profile. This needs a proper structured extraction pass, not substring checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The extraction call adds latency.&lt;/strong&gt; Every &lt;code&gt;sendMessage()&lt;/code&gt; triggers two Groq calls: one for the reply, one for extraction. On Groq's free tier this is fast enough not to notice, but it's worth being aware of if you're on a slower model or a paid tier with rate limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons that carry over
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Memory position in the prompt matters more than you'd expect.&lt;/strong&gt; Putting recalled context at the top of the system prompt — before behavioral instructions — made the agent treat it as ground truth. At the bottom it felt like a suggestion.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two LLM calls per turn is a reasonable pattern.&lt;/strong&gt; One call for the user-facing response, one narrow extraction call for structured fact parsing. The extraction prompt is tight and cheap. Trying to combine both into a single call makes the main prompt unwieldy and the extraction unreliable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fallback made development faster.&lt;/strong&gt; Designing the localStorage degradation path first meant we never blocked on API keys during development. The &lt;a href="https://hindsight.vectorize.io/" rel="noopener noreferrer"&gt;Hindsight documentation&lt;/a&gt; covers the memory bank setup well — but having a local fallback meant we could ship a working demo before we'd finished configuring the cloud side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Natural language beats raw JSON for memory storage.&lt;/strong&gt; Hindsight's &lt;a href="https://vectorize.io/features/agent-memory" rel="noopener noreferrer"&gt;agent memory&lt;/a&gt; does semantic indexing on ingestion. Storing &lt;code&gt;"Goldman Sachs (SWE Intern) - rejected"&lt;/code&gt; as part of a prose block gives the embedding model something to work with. Storing &lt;code&gt;{"company":"Goldman","status":"rejected"}&lt;/code&gt; would retrieve correctly but read awkwardly when injected back into the system prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Same agent, better input.&lt;/strong&gt; We didn't touch the recommendation logic between the stateless version and the memory-backed version. The improvement came entirely from what the agent received before it started talking. That's the cleaner mental model: memory isn't a feature of the agent — it's better input.&lt;/p&gt;




&lt;p&gt;The memory integration is built on &lt;a href="https://github.com/vectorize-io/hindsight" rel="noopener noreferrer"&gt;Hindsight&lt;/a&gt;. If you're building an agent that talks to the same user more than once, persistent memory is worth the integration cost. The alternative is an advisor that asks students to re-introduce themselves every single session — and recommends the company that just rejected them. You can browse the &lt;a href="https://github.com/Jagan83411/career-advisor" rel="noopener noreferrer"&gt;full project source here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/vectorize-io/hindsight" rel="noopener noreferrer"&gt;Hindsight GitHub repository&lt;/a&gt;&lt;/strong&gt; — the open-source memory layer we used. Start here if you want to add persistent agent memory to your own project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://hindsight.vectorize.io/" rel="noopener noreferrer"&gt;Hindsight documentation&lt;/a&gt;&lt;/strong&gt; — covers memory bank setup, the retain/recall API, metadata filtering, and how semantic indexing works under the hood.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://vectorize.io/features/agent-memory" rel="noopener noreferrer"&gt;Agent memory on Vectorize&lt;/a&gt;&lt;/strong&gt; — overview of how Hindsight handles long-term agent memory, useful if you're evaluating it against other approaches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/Jagan83411/career-advisor" rel="noopener noreferrer"&gt;Project repo&lt;/a&gt;&lt;/strong&gt; — the full source for this career advisor app, including &lt;code&gt;advisorService.js&lt;/code&gt; and the localStorage fallback.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>react</category>
      <category>agentmemory</category>
    </item>
  </channel>
</rss>
