v0.6.1.0 feat(assistant): twin profile + memory context (#147)#150
Merged
Conversation
…mpt (#147) Phase 2b of issue #135 — closes #147. The assistant now reads the user's twin profile (preferences + inferences above the moderate confidence floor) and recent episodic memories before composing each reply. Two ports keep @skytwin/assistant free of @skytwin/db / @skytwin/mempalace deps; adapters wire to real backings in apps/api/src/routes/assistant.ts. ContextBuilder - Hard cap at MAX_CONTEXT_BYTES = 2000 with UTF-8-clean ellipsis truncation. Noisy profiles can't dominate the token budget. - Confidence floor: only confirmed/high/moderate surface. Speculative + low entries stay in the model but don't broadcast. - Confidence-ranked truncation: highest-confidence wins MAX_PREFERENCES=12 / MAX_INFERENCES=6 / MAX_MEMORIES=5 slots. - Booleans render as yes/no for readability. - Partial-context fallback on either provider failure. - Empty-on-both-empty so AssistantService can short-circuit unchanged. AssistantService.reply now takes optional enrichment={userId, query}. Backward-compatible: omitting it OR omitting the ctor builder falls back to the bare default system prompt — phase 1 callers untouched. Wiring - TwinContextProvider: TwinService.getOrCreateProfile + userRepository.findById in parallel for preferences/inferences/trust-tier. - MemoryContextProvider: query → ≥3-char tokens → mempalaceRepository.searchEpisodes (same backing as MemoryStack L3). - Episode outcome JSON collapses to a one-line label (kind/status/result field if present, else short stringification). Tests: 15 new (12 ContextBuilder + 4 AssistantService enrichment paths). Full suite green across 40 packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds phase 2b assistant enrichment so @skytwin/assistant can prepend twin-profile facts and relevant episodic memories to the system prompt before generating a reply. This fits the assistant pipeline by keeping enrichment logic package-local/testable while wiring real data sources in the API route.
Changes:
- Added a new
ContextBuilderplus exported port/types for twin-profile and memory-context enrichment. - Extended
AssistantService.reply()with optional enrichment input and prepended context support. - Wired the API assistant route to build/query twin + mempalace context, and added unit tests/changelog/version updates.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
VERSION |
Bumps release to 0.6.1.0. |
packages/assistant/src/index.ts |
Exports new enrichment types and ContextBuilder. |
packages/assistant/src/context-builder.ts |
Implements context rendering, filtering, truncation, and provider fallback. |
packages/assistant/src/assistant-service.ts |
Adds optional enrichment flow to assistant reply generation. |
packages/assistant/src/__tests__/context-builder.test.ts |
Covers context rendering/truncation/fallback behavior. |
packages/assistant/src/__tests__/assistant-service.test.ts |
Covers enrichment prompt-prepending behavior in the service. |
CHANGELOG.md |
Documents the new assistant enrichment release. |
apps/api/src/routes/assistant.ts |
Wires real twin/memory providers into the assistant API route. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+257
to
+259
| for (const i of infs) { | ||
| const reason = i.reasoning ? ` — ${i.reasoning}` : ''; | ||
| lines.push(`- ${i.domain}/${i.key} = ${renderValue(i.value)} (${i.confidence})${reason}`); |
Comment on lines
+129
to
+132
| const terms = query | ||
| .toLowerCase() | ||
| .split(/[^a-z0-9]+/i) | ||
| .filter((t) => t.length >= 3); |
| async search(userId, query, limit = 5) { | ||
| const terms = query | ||
| .toLowerCase() | ||
| .split(/[^a-z0-9]+/i) |
| - `MemoryContextProvider` adapter splits the query into ≥3-char tokens and calls `mempalaceRepository.searchEpisodes` — same backing call that `MemoryStack.search` uses for L3 deep-search. Stop-words and short tokens drop so a query like "the plan for X" doesn't ILIKE-match every episode containing "the". | ||
| - Episode `outcome` JSON blobs collapse to a one-line label (`kind` / `status` / `result` field if present, else short stringification) so the rendered context stays compact. | ||
|
|
||
| ### Tests (15 new) |
| let reply; | ||
| try { | ||
| reply = await service.reply(history); | ||
| reply = await service.reply(history, { userId, query: content }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #147 (assistant phase 2b). The conversational assistant now reads the user's twin profile (preferences + inferences) and recent episodic memories before composing each reply, so it can answer the two killer use cases that distinguish a personal twin from generic ChatGPT:
What landed
ContextBuilderin@skytwin/assistantComposes a compact context block prepended to the system prompt:
```
What I know about you
Trust tier: moderate_autonomy
Preferences:
Inferences (not yet user-confirmed):
Relevant past episodes
```
Two ports keep the
@skytwin/assistantpackage free of@skytwin/db/@skytwin/mempalacedeps and unit-testable with stubs:TwinContextProvider.fetchTwinService.getOrCreateProfile+userRepository.findByIdMemoryContextProvider.searchmempalaceRepository.searchEpisodesDesign choices:
confirmed/high/moderatesurface. Speculative + low entries stay in the twin model but don't broadcast (would make the assistant look unsure of itself).MAX_PREFERENCES = 12/MAX_INFERENCES = 6/MAX_MEMORIES = 5slots.console.warnrecords which side failed; the request continues with whatever was retrievable.'', whichAssistantServicetreats as "use the default system prompt unchanged" — same behavior as phase 1, no surprises for early bring-up paths without context.AssistantService.reply()now takes optional enrichmentNew optional 2nd param
enrichment?: { userId, query }. Backward-compatible: omitting it OR omitting the ctor builder falls back to the bare default system prompt — phase 1 callers compile unchanged.Wiring
apps/api/src/routes/assistant.tsconstructs aContextBuilderonce per process.enrichment.queryis the just-sent user message — that's what the assistant is about to answer, so the most-relevant memories are the ones that match it.mempalaceRepository.searchEpisodes— same backing asMemoryStack.searchL3. Stop-words and short tokens drop so a query like "the plan for X" doesn't ILIKE-match every episode containing "the".outcomeJSON blobs collapse to a one-line label (kind/status/resultfield if present, else short stringification) so the rendered context stays compact.Safety / invariants
sourceandevidence), and episodic memorysummarystrings are written by the system itself, not by external senders. So even though we expand what the LLM sees, we don't expand the attack surface.userIdaccess; the new context endpoint isn't a thing — context is built server-side per request.Promise.all.Test plan
pnpm build— 20 packages greenpnpm test— 40 packages green; 15 new tests (12 ContextBuilder + 4 AssistantService enrichment paths) on top of phase-1's 24pnpm lint— clean#/assistant, ask the twin about its preferences (e.g. "do I auto-archive emails?") and verify it answers from the profile, not from generic priorsPhase 2+ still in flight
LlmClientAPI🤖 Generated with Claude Code