ResolveIQ is a support workflow built around that distinction. It combines ordinary operational records in PostgreSQL, a language model for drafting replies, and Hindsight for customer-level memory across conversations. The through-line is simple: a proposed fix is not a resolution. We need to know what was tried, whether it worked, and what the next response should avoid.
A support agent can sound attentive while making a customer repeat the same troubleshooting step for the third time. I wanted the system to remember the outcome, not just the conversation—and to make that memory useful on the next ticket.
The system around the conversation
The application has a React and TypeScript frontend, a FastAPI backend, and PostgreSQL records for customers, tickets, conversations, messages, resolution attempts, and escalations. The support agent uses a server-side Groq client to generate a response. Hindsight provides retain, recall, and reflect operations for customer memories.
I keep those responsibilities separate. PostgreSQL owns facts that need explicit structure and predictable updates: the ticket state, the assignment, the message history, and each attempted solution’s outcome. Hindsight owns the customer memory that should inform a future interaction without turning every memory into a hand-maintained relational schema.
Each customer gets a distinct Hindsight bank. The service derives its ID from a configured prefix and customer ID:
def bank_id(self, customer_id: int) -> str: return f"{self.settings.hindsight_bank_prefix}-customer-{customer_id}" That boundary matters. A memory about one customer’s billing history should not be available while answering another customer’s ticket. It also gives the application a clear ownership rule: the ticket database is the system of record for workflow; Hindsight is the system used to retain and retrieve useful customer context.
The design uses Hindsight, an open-source agent memory system, through its Python client. Its retain, recall, and reflect documentation maps cleanly to the lifecycle here: retain an interaction, recall relevant context before drafting, and reflect when a support agent wants a synthesized view of the customer’s history. That is one practical version of agent memory for applications: persistence that participates in the next decision, rather than a transcript archive nobody consults.
The important unit is an attempt
The central design decision was to represent a proposed solution as a resolution attempt with an outcome. The model’s answer alone is not enough. The system records a proposed fix as pending; a later response can mark that pending attempt failed or succeeded. Human agents can also add and update attempts.
That gives the next generation call two useful sources of context. The recent conversation explains what the customer said. The structured attempt list explains what the support process has already tried and whether it worked. Customer memory adds context that may span tickets: for example, recurring account conditions or an earlier resolution worth reusing.
The prompt builder puts those sources into distinct sections instead of flattening everything into a vague summary:
return ( f"{SYSTEM_INSTRUCTIONS}\n\n" f"--- CURRENT CONTEXT ---\n" f"{customer_info}\n\n" f"Customer Memories:\n{memories_str}\n\n" f"Prior Attempted Solutions on this Ticket (DO NOT REPEAT FAILED ONES):\n" f"{attempts_str}\n" ) This is intentionally explicit. A failed password reset and a customer preference are different kinds of information, and they should not be interchangeable just because both appear in prose. Attempts are auditable workflow records. Hindsight memories are retrieved context. The model gets both, with clear instructions about the action it must take.
I would not describe this as a mathematical guarantee that a model can never repeat a failed fix. A model can misunderstand context or produce an unexpected response. The engineering goal is narrower and testable: preserve the outcomes, place failed attempts in front of the model, instruct it not to repeat them, and keep the proposed next action visible to support staff. If a hard prohibition is required for a particular class of operation, enforce that in application logic rather than trusting prompt wording.
Retrieve, reason, record
For each reply, the backend saves the incoming customer message first. It then recalls Hindsight memories using a query formed from the ticket subject and current message. The recalled results and recent conversation are passed to the model. After generation, the backend separates customer-facing text from a metadata block, updates the ticket and attempt records, retains the exchange in Hindsight, and evaluates escalation policies.
The memory call is kept out of the event loop’s blocking path. The service wraps the synchronous SDK call with asyncio.to_thread and updates its status only after a successful call:
async def _run(self, fn): try: result = await asyncio.to_thread(fn) except MemoryNotConfigured: raise except Exception as exc: self.last_error = f"{type(exc).__name__}: {exc}"[:300] raise MemoryServiceError(self.last_error) from exc self.last_success_at = utcnow() self.last_error = None return result That wrapper gives the API a consistent boundary around configuration and service failures. A missing or unavailable memory service should be observable as such; the application should not quietly pretend that recall happened. In a production deployment, I would pair this boundary with timeouts, retry policy appropriate to the request path, and operational alerts based on memory availability.
The streaming path follows the same sequence while emitting status and response deltas over server-sent events. It holds back a short suffix of generated text so a metadata delimiter split across token chunks is not accidentally shown to the customer. Once the model’s metadata block begins, it accumulates that separately. The customer sees the reply as it arrives; the backend still gets structured fields to record the proposed solution, intent, category, and priority.
A concrete interaction
Imagine a customer whose card keeps failing during checkout. On the first ticket, the agent suggests removing and re-adding the card. The backend stores that proposal as a pending attempt. The customer replies that it did not work, and the next update marks the attempt failed. The exchange is also retained in that customer’s Hindsight bank.
Later, the customer opens a related ticket and says checkout still fails. The agent recalls relevant customer context, reads the new ticket’s attempt history, and gets the failed card re-add step in a dedicated prompt section. Its instructions tell it to propose a fresh alternative, acknowledge the failed step, and avoid claiming it has performed irreversible actions such as issuing a refund or resetting an account. The draft’s proposed fix is recorded as a new pending attempt, ready for an agent or customer outcome to resolve.
This combination is more useful than relying on memory alone. The ticket’s attempt rows provide precise, inspectable outcomes for that issue. Hindsight can surface relevant context across tickets without requiring the application to copy and query every old conversation itself. The model receives a compact working context; the application retains the durable operational facts.
If attempts continue to fail, escalation rules can create an internal review record. The policy engine can also flag repeated complaints, unresolved tickets past a configured age, and high-priority issues. The escalation snapshot includes the ticket history, attempts, customer information, and related tickets so a human can pick up the case with context. In a complete deployment, those policies and routing integrations should be backed by operational monitoring and clear ownership; creating a record is only the first step in a reliable handoff.
What I learned
1. Separate memory from workflow state
A semantic memory store is useful for retrieving context, but it should not become the only place that records whether a fix failed. Outcomes need explicit fields, audit history, and predictable updates. Use the memory layer to help find context; use the relational database for decisions the workflow must enforce and explain.
2. Persist outcomes, not just transcripts
A long conversation is not a reliable representation of what the support process tried. Explicit resolution attempts make the failure legible to both the next model call and a human agent. If the outcome is unknown, represent it as unknown or pending rather than inferring success from silence.
3. Make memory failures visible
A system that silently substitutes “no memories” when recall fails is difficult to debug and can produce misleading confidence. Expose whether recall and retain succeeded, track the last successful call and last error, and design the product’s fallback behavior intentionally. Memory is a dependency with failure modes, not a prompt decoration.
4. Keep model output inside application boundaries
The model can suggest a category, priority, and solution, but the backend validates categories and only allows priority to rise. It records the proposal as pending instead of treating generated text as completed work. That separation is essential whenever a reply can affect customer expectations or downstream operations.
5. Use memory to reduce repetition, then verify the result
Recall can help an agent avoid asking the customer to start over. It cannot prove a fix worked. The system still needs a follow-up outcome, whether from the customer or a human agent, before a suggestion becomes a successful resolution. That feedback is what makes the next interaction better grounded.
The useful promise of this design is not that a language model will never make a mistake. It is that the support system can remember what it tried, carry the outcome forward, and make the next response answerable to that history. Hindsight supplies the cross-ticket context; explicit attempt records keep the workflow honest. That is the foundation I would want before trusting an AI agent with a customer who has already heard “try it again.”




