Skip to content

Commit 07f706a

Browse files
committed
feat(extended-memory): implement P3-P5 anticipatory layer
- P3: user-state model with pending-review queue and background inference - P4: predictive intent generation, atom associations, predictive recall augmentation - P5: return-after-break summaries, anaphora resolution, follow-up anticipation, style mirroring - Add memory tool actions and CLI commands for pending review, promotion, and pinning - Update docs in MEMORY.md, EXTENDED_MEMORY.md, and CONFIG.md
1 parent 0ca3095 commit 07f706a

16 files changed

Lines changed: 2793 additions & 116 deletions

cmd/odek/main.go

Lines changed: 157 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -254,15 +254,15 @@ type runFlags struct {
254254
NoAgents *bool // nil = not set
255255
PromptCaching *bool // nil = not set; true = enable prompt caching
256256
Session *bool // nil = not set; true = save session after run
257-
Learn *bool // nil = not set; true = enable skills learning mode
258-
Task string
257+
Learn *bool // nil = not set; true = enable skills learning mode
258+
Task string
259259

260260
// ToolsEnabled and ToolsDisabled control which tools are exposed to the LLM.
261261
// Repeated --tool/--no-tool flags accumulate. They are the highest priority
262262
// layer after file config and env vars.
263263
ToolsEnabled []string
264264
ToolsDisabled []string
265-
Ctx []string // --ctx files to attach
265+
Ctx []string // --ctx files to attach
266266

267267
// Sandbox-specific CLI flags
268268
SandboxImage string // Docker image (e.g. "node:20-alpine")
@@ -273,10 +273,18 @@ type runFlags struct {
273273
SandboxReadonly *bool // nil = not set; true = read-only mount
274274

275275
// Extended memory subsystem CLI overrides.
276-
MemoryExtendedEnabled *bool // nil = not set
277-
MemoryExtendedMaxSizeMB int // 0 = not set
278-
MemoryExtendedAtomMaxChars int // 0 = not set
279-
MemoryExtendedMemoryBudgetChars int // 0 = not set
276+
MemoryExtendedEnabled *bool // nil = not set
277+
MemoryExtendedMaxSizeMB int // 0 = not set
278+
MemoryExtendedAtomMaxChars int // 0 = not set
279+
MemoryExtendedMemoryBudgetChars int // 0 = not set
280+
MemoryExtendedUserStateTurnInterval int // 0 = not set
281+
MemoryExtendedUserStateMaxPending int // 0 = not set
282+
MemoryExtendedAssociationsEnabled *bool // nil = not set
283+
MemoryExtendedAssociationSemanticTopK int // 0 = not set
284+
MemoryExtendedProactiveReturnAfterBreak *bool // nil = not set
285+
MemoryExtendedStyleMirroringEnabled *bool // nil = not set
286+
MemoryExtendedAnaphoraResolutionEnabled *bool // nil = not set
287+
MemoryExtendedFollowUpAnticipationEnabled *bool // nil = not set
280288

281289
Deliver *bool // nil = not set; true = deliver result to default channel
282290
}
@@ -430,6 +438,54 @@ func parseRunFlags(args []string) (runFlags, error) {
430438
}
431439
fmt.Sscanf(args[i+1], "%d", &f.MemoryExtendedMemoryBudgetChars)
432440
i += 2
441+
case "--memory-extended-user-state-turn-interval":
442+
if i+1 >= len(args) {
443+
return f, fmt.Errorf("--memory-extended-user-state-turn-interval requires a value")
444+
}
445+
fmt.Sscanf(args[i+1], "%d", &f.MemoryExtendedUserStateTurnInterval)
446+
i += 2
447+
case "--memory-extended-user-state-max-pending":
448+
if i+1 >= len(args) {
449+
return f, fmt.Errorf("--memory-extended-user-state-max-pending requires a value")
450+
}
451+
fmt.Sscanf(args[i+1], "%d", &f.MemoryExtendedUserStateMaxPending)
452+
i += 2
453+
case "--memory-extended-associations-enabled":
454+
f.MemoryExtendedAssociationsEnabled = boolPtr(true)
455+
i++
456+
case "--memory-extended-associations-disabled":
457+
f.MemoryExtendedAssociationsEnabled = boolPtr(false)
458+
i++
459+
case "--memory-extended-association-semantic-top-k":
460+
if i+1 >= len(args) {
461+
return f, fmt.Errorf("--memory-extended-association-semantic-top-k requires a value")
462+
}
463+
fmt.Sscanf(args[i+1], "%d", &f.MemoryExtendedAssociationSemanticTopK)
464+
i += 2
465+
case "--memory-extended-proactive-return-after-break":
466+
f.MemoryExtendedProactiveReturnAfterBreak = boolPtr(true)
467+
i++
468+
case "--memory-extended-no-proactive-return-after-break":
469+
f.MemoryExtendedProactiveReturnAfterBreak = boolPtr(false)
470+
i++
471+
case "--memory-extended-style-mirroring-enabled":
472+
f.MemoryExtendedStyleMirroringEnabled = boolPtr(true)
473+
i++
474+
case "--memory-extended-style-mirroring-disabled":
475+
f.MemoryExtendedStyleMirroringEnabled = boolPtr(false)
476+
i++
477+
case "--memory-extended-anaphora-resolution-enabled":
478+
f.MemoryExtendedAnaphoraResolutionEnabled = boolPtr(true)
479+
i++
480+
case "--memory-extended-anaphora-resolution-disabled":
481+
f.MemoryExtendedAnaphoraResolutionEnabled = boolPtr(false)
482+
i++
483+
case "--memory-extended-follow-up-anticipation-enabled":
484+
f.MemoryExtendedFollowUpAnticipationEnabled = boolPtr(true)
485+
i++
486+
case "--memory-extended-follow-up-anticipation-disabled":
487+
f.MemoryExtendedFollowUpAnticipationEnabled = boolPtr(false)
488+
i++
433489
case "--deliver":
434490
f.Deliver = boolPtr(true)
435491
i++
@@ -678,10 +734,23 @@ Sandbox flags:
678734
--sandbox-user <s> Run as user (uid:gid or name)
679735
680736
Extended memory flags:
681-
--memory-extended-enabled Enable Extended Memory (opt-in)
682-
--memory-extended-max-size-mb <n> Max on-disk size in MiB (default: 100)
683-
--memory-extended-atom-max-chars <n> Max chars per atom (default: 300)
684-
--memory-extended-memory-budget-chars <n> Max chars injected into prompt (default: 2000)
737+
--memory-extended-enabled Enable Extended Memory (opt-in)
738+
--memory-extended-max-size-mb <n> Max on-disk size in MiB (default: 100)
739+
--memory-extended-atom-max-chars <n> Max chars per atom (default: 300)
740+
--memory-extended-memory-budget-chars <n> Max chars injected into prompt (default: 2000)
741+
--memory-extended-user-state-turn-interval <n> Turns between user-state inferences (default: 5)
742+
--memory-extended-user-state-max-pending <n> Max pending user-state inferences (default: 20)
743+
--memory-extended-associations-enabled Enable atom associations (default: true)
744+
--memory-extended-associations-disabled Disable atom associations
745+
--memory-extended-association-semantic-top-k <n> Semantic neighbours per atom (default: 3)
746+
--memory-extended-proactive-return-after-break Resume summary on continue (default: true)
747+
--memory-extended-no-proactive-return-after-break Disable resume summary
748+
--memory-extended-style-mirroring-enabled Mirror inferred user style (default: true)
749+
--memory-extended-style-mirroring-disabled Disable style mirroring
750+
--memory-extended-anaphora-resolution-enabled Resolve pronouns against atoms (default: true)
751+
--memory-extended-anaphora-resolution-disabled Disable anaphora resolution
752+
--memory-extended-follow-up-anticipation-enabled Pre-load follow-up context (default: true)
753+
--memory-extended-follow-up-anticipation-disabled Disable follow-up anticipation
685754
686755
Config sources (lowest to highest priority):
687756
~/.odek/config.json Global defaults (shared across projects)
@@ -710,7 +779,15 @@ Environment variables:
710779
ODEK_MEMORY_EXTENDED_ENABLED true/false — enable Extended Memory
711780
ODEK_MEMORY_EXTENDED_MAX_SIZE_MB Max on-disk size in MiB
712781
ODEK_MEMORY_EXTENDED_ATOM_MAX_CHARS Max chars per atom
713-
ODEK_MEMORY_EXTENDED_MEMORY_BUDGET_CHARS Max chars injected into prompt`)
782+
ODEK_MEMORY_EXTENDED_MEMORY_BUDGET_CHARS Max chars injected into prompt
783+
ODEK_MEMORY_EXTENDED_USER_STATE_TURN_INTERVAL Turns between user-state inferences
784+
ODEK_MEMORY_EXTENDED_USER_STATE_MAX_PENDING Max pending user-state inferences
785+
ODEK_MEMORY_EXTENDED_ASSOCIATIONS_ENABLED true/false — enable atom associations
786+
ODEK_MEMORY_EXTENDED_ASSOCIATION_SEMANTIC_TOP_K Semantic neighbours per atom
787+
ODEK_MEMORY_EXTENDED_PROACTIVE_RETURN_AFTER_BREAK true/false — resume summary on continue
788+
ODEK_MEMORY_EXTENDED_STYLE_MIRRORING_ENABLED true/false — mirror inferred user style
789+
ODEK_MEMORY_EXTENDED_ANAPHORA_RESOLUTION_ENABLED true/false — resolve pronouns against atoms
790+
ODEK_MEMORY_EXTENDED_FOLLOW_UP_ANTICIPATION_ENABLED true/false — pre-load follow-up context`)
714791
}
715792

716793
// ── Init ──────────────────────────────────────────────────────────────
@@ -909,10 +986,18 @@ func run(args []string) error {
909986
SandboxCPUs: f.SandboxCPUs,
910987
SandboxUser: f.SandboxUser,
911988

912-
MemoryExtendedEnabled: f.MemoryExtendedEnabled,
913-
MemoryExtendedMaxSizeMB: f.MemoryExtendedMaxSizeMB,
914-
MemoryExtendedAtomMaxChars: f.MemoryExtendedAtomMaxChars,
915-
MemoryExtendedMemoryBudgetChars: f.MemoryExtendedMemoryBudgetChars,
989+
MemoryExtendedEnabled: f.MemoryExtendedEnabled,
990+
MemoryExtendedMaxSizeMB: f.MemoryExtendedMaxSizeMB,
991+
MemoryExtendedAtomMaxChars: f.MemoryExtendedAtomMaxChars,
992+
MemoryExtendedMemoryBudgetChars: f.MemoryExtendedMemoryBudgetChars,
993+
MemoryExtendedUserStateTurnInterval: f.MemoryExtendedUserStateTurnInterval,
994+
MemoryExtendedUserStateMaxPending: f.MemoryExtendedUserStateMaxPending,
995+
MemoryExtendedAssociationsEnabled: f.MemoryExtendedAssociationsEnabled,
996+
MemoryExtendedAssociationSemanticTopK: f.MemoryExtendedAssociationSemanticTopK,
997+
MemoryExtendedProactiveReturnAfterBreak: f.MemoryExtendedProactiveReturnAfterBreak,
998+
MemoryExtendedStyleMirroringEnabled: f.MemoryExtendedStyleMirroringEnabled,
999+
MemoryExtendedAnaphoraResolutionEnabled: f.MemoryExtendedAnaphoraResolutionEnabled,
1000+
MemoryExtendedFollowUpAnticipationEnabled: f.MemoryExtendedFollowUpAnticipationEnabled,
9161001
})
9171002

9181003
// Resolve @references and --ctx file attachments in the task
@@ -1022,18 +1107,18 @@ func run(args []string) error {
10221107
SystemMessage: systemMessage,
10231108
UntrustedWrapper: func(source, content string) string { return wrapUntrusted(context.Background(), source, content) },
10241109
NoProjectFile: resolved.NoAgents,
1025-
Thinking: resolved.Thinking,
1026-
ThinkingBudget: f.ThinkingBudget,
1027-
Temperature: 0, // deterministic by default; override with --temperature
1028-
Tools: tools,
1029-
ToolFilter: odek.ToolFilterConfig{Enabled: resolved.Tools.Enabled, Disabled: resolved.Tools.Disabled},
1030-
SandboxCleanup: sandboxCleanup,
1031-
Renderer: rend,
1032-
Skills: skillsCfg,
1033-
SkillManager: sm,
1034-
PromptCaching: resolved.PromptCaching,
1035-
MemoryDir: expandHome("~/.odek/memory"),
1036-
MemoryConfig: resolved.Memory,
1110+
Thinking: resolved.Thinking,
1111+
ThinkingBudget: f.ThinkingBudget,
1112+
Temperature: 0, // deterministic by default; override with --temperature
1113+
Tools: tools,
1114+
ToolFilter: odek.ToolFilterConfig{Enabled: resolved.Tools.Enabled, Disabled: resolved.Tools.Disabled},
1115+
SandboxCleanup: sandboxCleanup,
1116+
Renderer: rend,
1117+
Skills: skillsCfg,
1118+
SkillManager: sm,
1119+
PromptCaching: resolved.PromptCaching,
1120+
MemoryDir: expandHome("~/.odek/memory"),
1121+
MemoryConfig: resolved.Memory,
10371122
})
10381123
if err != nil {
10391124
return err
@@ -1994,27 +2079,27 @@ func continueCmd(args []string) error {
19942079
skillsCfg = &resolved.Skills
19952080
}
19962081

1997-
agent, err := odek.New(odek.Config{
1998-
Model: resolved.Model,
1999-
BaseURL: resolved.BaseURL,
2000-
APIKey: resolved.APIKey,
2001-
MaxIterations: resolved.MaxIter,
2002-
MaxToolParallel: resolved.MaxToolParallel,
2003-
SystemMessage: systemMessage,
2004-
UntrustedWrapper: func(source, content string) string { return wrapUntrusted(context.Background(), source, content) },
2005-
NoProjectFile: resolved.NoAgents,
2006-
Thinking: resolved.Thinking,
2007-
Temperature: 0, // deterministic by default; override with --temperature
2008-
Tools: tools,
2009-
ToolFilter: odek.ToolFilterConfig{Enabled: resolved.Tools.Enabled, Disabled: resolved.Tools.Disabled},
2010-
SandboxCleanup: sandboxCleanup,
2011-
Renderer: rend,
2012-
Skills: skillsCfg,
2013-
SkillManager: sm,
2014-
PromptCaching: resolved.PromptCaching,
2015-
MemoryDir: expandHome("~/.odek/memory"),
2016-
MemoryConfig: resolved.Memory,
2017-
})
2082+
agent, err := odek.New(odek.Config{
2083+
Model: resolved.Model,
2084+
BaseURL: resolved.BaseURL,
2085+
APIKey: resolved.APIKey,
2086+
MaxIterations: resolved.MaxIter,
2087+
MaxToolParallel: resolved.MaxToolParallel,
2088+
SystemMessage: systemMessage,
2089+
UntrustedWrapper: func(source, content string) string { return wrapUntrusted(context.Background(), source, content) },
2090+
NoProjectFile: resolved.NoAgents,
2091+
Thinking: resolved.Thinking,
2092+
Temperature: 0, // deterministic by default; override with --temperature
2093+
Tools: tools,
2094+
ToolFilter: odek.ToolFilterConfig{Enabled: resolved.Tools.Enabled, Disabled: resolved.Tools.Disabled},
2095+
SandboxCleanup: sandboxCleanup,
2096+
Renderer: rend,
2097+
Skills: skillsCfg,
2098+
SkillManager: sm,
2099+
PromptCaching: resolved.PromptCaching,
2100+
MemoryDir: expandHome("~/.odek/memory"),
2101+
MemoryConfig: resolved.Memory,
2102+
})
20182103
if err != nil {
20192104
return err
20202105
}
@@ -2035,6 +2120,29 @@ func continueCmd(args []string) error {
20352120
// Build message history: session messages + new user message
20362121
// The system message is already in the session
20372122
messages := sess.GetMessages()
2123+
2124+
// Return-after-break: on session resume, load a concise summary of where
2125+
// the user left off and the next likely step.
2126+
if mm := agent.Memory(); mm != nil {
2127+
rbCtx, rbCancel := context.WithTimeout(context.Background(), 5*time.Second)
2128+
if rb := mm.FormatReturnAfterBreak(rbCtx); rb != "" {
2129+
insertIdx := -1
2130+
for i := len(messages) - 1; i >= 0; i-- {
2131+
if messages[i].Role == "system" {
2132+
insertIdx = i
2133+
break
2134+
}
2135+
}
2136+
rbMsg := llm.Message{Role: "system", Content: rb}
2137+
if insertIdx >= 0 {
2138+
messages = append(messages[:insertIdx+1], append([]llm.Message{rbMsg}, messages[insertIdx+1:]...)...)
2139+
} else {
2140+
messages = append([]llm.Message{rbMsg}, messages...)
2141+
}
2142+
}
2143+
rbCancel()
2144+
}
2145+
20382146
messages = append(messages, llm.Message{Role: "user", Content: task})
20392147

20402148
// Append user input to buffer (AppendBuffer summarizes raw text).

cmd/odek/memory_cmd.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func memoryCmd(args []string) error {
7373
// extendedMemoryCmd handles `odek memory extended forget|quarantine|compact`.
7474
func extendedMemoryCmd(dir string, args []string) error {
7575
if len(args) == 0 {
76-
fmt.Fprintf(os.Stderr, "Usage: odek memory extended <forget|promote|pin|quarantine|compact> [args]\n")
76+
fmt.Fprintf(os.Stderr, "Usage: odek memory extended <forget|promote|pin|quarantine|compact|pending|confirm|reject> [args]\n")
7777
return nil
7878
}
7979

@@ -140,7 +140,48 @@ func extendedMemoryCmd(dir string, args []string) error {
140140
fmt.Println("odek: Extended Memory vector index compaction triggered in the background")
141141
return nil
142142

143+
case "pending":
144+
pending, err := em.ListPendingReview()
145+
if err != nil {
146+
return err
147+
}
148+
if len(pending) == 0 {
149+
fmt.Println("No pending user-model reviews.")
150+
return nil
151+
}
152+
fmt.Printf("%d pending review(s):\n\n", len(pending))
153+
for _, p := range pending {
154+
fmt.Printf("• %s | %s = %q (confidence %.2f)\n", p.ID, p.Field, truncate(p.Value, 120), p.Confidence)
155+
if p.Evidence != "" {
156+
fmt.Printf(" evidence: %s\n", truncate(p.Evidence, 120))
157+
}
158+
}
159+
fmt.Println("\nConfirm with: odek memory extended confirm <id>")
160+
return nil
161+
162+
case "confirm":
163+
if len(subArgs) == 0 {
164+
return fmt.Errorf("usage: odek memory extended confirm <pending_id>")
165+
}
166+
id := subArgs[0]
167+
if err := em.ConfirmPendingReview(id); err != nil {
168+
return err
169+
}
170+
fmt.Printf("odek: confirmed pending review %q\n", id)
171+
return nil
172+
173+
case "reject":
174+
if len(subArgs) == 0 {
175+
return fmt.Errorf("usage: odek memory extended reject <pending_id>")
176+
}
177+
id := subArgs[0]
178+
if err := em.RejectPendingReview(id); err != nil {
179+
return err
180+
}
181+
fmt.Printf("odek: rejected pending review %q\n", id)
182+
return nil
183+
143184
default:
144-
return fmt.Errorf("unknown extended memory subcommand %q (expected: forget, promote, pin, quarantine, compact)", sub)
185+
return fmt.Errorf("unknown extended memory subcommand %q (expected: forget, promote, pin, quarantine, compact, pending, confirm, reject)", sub)
145186
}
146187
}

0 commit comments

Comments
 (0)