Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions packages/types/src/__tests__/learning-memory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// npx vitest run src/__tests__/learning-memory.test.ts

import {
DEFAULT_LEARNING_CONFIG,
EMPTY_LEARNING_STATE,
learningConfigSchema,
learningStateSchema,
memoryContextSchema,
type LearningState,
type MemoryContext,
} from "../index.js"

describe("learning types", () => {
it("exports the default learning config", () => {
expect(DEFAULT_LEARNING_CONFIG).toMatchObject({
enabled: false,
reviewOnTurnCount: 10,
reviewOnToolIterationCount: 50,
})
})

it("parses the empty learning state", () => {
const result = learningStateSchema.safeParse(EMPTY_LEARNING_STATE)

expect(result.success).toBe(true)
expect(result.data).toEqual(EMPTY_LEARNING_STATE)
})

it("applies learning config defaults", () => {
const result = learningConfigSchema.parse({})

expect(result).toEqual(DEFAULT_LEARNING_CONFIG)
})

it("preserves TypeScript inference for learning state", () => {
const state: LearningState = EMPTY_LEARNING_STATE

expect(state.version).toBe(1)
})
})

describe("memory types", () => {
it("parses a valid memory context", () => {
const input: MemoryContext = {
entries: [],
revision: 0,
generatedAt: Date.now(),
}

const result = memoryContextSchema.safeParse(input)

expect(result.success).toBe(true)
expect(result.data).toEqual(input)
})

it("rejects more than ten memory entries", () => {
const result = memoryContextSchema.safeParse({
entries: Array.from({ length: 11 }, (_, index) => ({
id: `entry-${index}`,
content: "memory",
source: "learning",
createdAt: index,
updatedAt: index,
})),
revision: 0,
generatedAt: Date.now(),
})

expect(result.success).toBe(false)
})
})
9 changes: 8 additions & 1 deletion packages/types/src/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
* ExperimentId
*/

export const experimentIds = ["preventFocusDisruption", "imageGeneration", "runSlashCommand", "customTools"] as const
export const experimentIds = [
"preventFocusDisruption",
"imageGeneration",
"runSlashCommand",
"customTools",
"selfImproving",
] as const

export const experimentIdsSchema = z.enum(experimentIds)

Expand All @@ -21,6 +27,7 @@ export const experimentsSchema = z.object({
imageGeneration: z.boolean().optional(),
runSlashCommand: z.boolean().optional(),
customTools: z.boolean().optional(),
selfImproving: z.boolean().optional(),
})

export type Experiments = z.infer<typeof experimentsSchema>
Expand Down
3 changes: 3 additions & 0 deletions packages/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ export * from "./global-settings.js"
export * from "./history.js"
export * from "./image-generation.js"
export * from "./ipc.js"
export * from "./learning.js"
export * from "./marketplace.js"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify duplicate star re-exports in packages/types/src/index.ts
rg -n '^export \* from "\./[^"]+\.js"$' packages/types/src/index.ts \
  | sed -E 's/.*"\.\/([^"]+)\.js".*/\1/' \
  | sort \
  | uniq -cd
# Expected: a count >1 for "marketplace"

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 85


🏁 Script executed:

#!/bin/bash
nl -ba packages/types/src/index.ts | sed -n '1,120p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 106


🏁 Script executed:

#!/bin/bash
# Show the relevant portion of packages/types/src/index.ts with line numbers
cat -n packages/types/src/index.ts | sed -n '1,80p'

Repository: Zoo-Code-Org/Zoo-Code

Length of output: 1466


Remove the duplicate marketplace re-export

packages/types/src/index.ts re-exports ./marketplace.js twice (lines 18 and 28), which is redundant.

♻️ Proposed fix
 export * from "./learning.js"
 export * from "./marketplace.js"
 export * from "./mcp.js"
 export * from "./message.js"
 export * from "./memory.js"
@@
 export * from "./skills.js"
-export * from "./marketplace.js"
 export * from "./telemetry.js"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/types/src/index.ts` at line 18, There is a duplicated re-export of
"./marketplace.js" — remove the duplicate export statement "export * from
\"./marketplace.js\"" so that only one re-export of marketplace remains in the
file; keep the remaining export as-is and run the type/build checks to ensure
nothing else relies on the duplicate.

export * from "./mcp.js"
export * from "./message.js"
export * from "./memory.js"
export * from "./mode.js"
export * from "./model.js"
export * from "./provider-settings.js"
Expand Down
193 changes: 193 additions & 0 deletions packages/types/src/learning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { z } from "zod"

/**
* FeedbackSignal - types of learning observations
*/
export const feedbackSignalSchema = z.enum([
"USER_CORRECTION",
"TASK_SUCCESS",
"TASK_FAILURE",
"PATTERN_REPEAT",
"CODE_INDEX_HIT",
"PROMPT_QUALITY",
])

export type FeedbackSignal = z.infer<typeof feedbackSignalSchema>

/**
* LearningConfig - configuration for the learning system
*/
export const learningConfigSchema = z.object({
enabled: z.boolean().default(false),
reviewOnTurnCount: z.number().int().min(1).default(10),
reviewOnToolIterationCount: z.number().int().min(1).default(50),
maxStoredPatterns: z.number().int().min(1).default(100),
maxStoredEvents: z.number().int().min(1).default(500),
maxPromptPatterns: z.number().int().min(1).default(5),
curatorEnabled: z.boolean().default(true),
curatorIntervalMs: z.number().int().min(60000).default(3600000),
staleAfterDays: z.number().int().min(1).default(14),
archiveAfterDays: z.number().int().min(1).default(60),
codeIndexCorrelationEnabled: z.boolean().default(true),
})

export type LearningConfig = z.infer<typeof learningConfigSchema>

export const DEFAULT_LEARNING_CONFIG: LearningConfig = {
enabled: false,
reviewOnTurnCount: 10,
reviewOnToolIterationCount: 50,
maxStoredPatterns: 100,
maxStoredEvents: 500,
maxPromptPatterns: 5,
curatorEnabled: true,
curatorIntervalMs: 3600000,
staleAfterDays: 14,
archiveAfterDays: 60,
codeIndexCorrelationEnabled: true,
}

/**
* LearningEvent - a single learning observation
*/
export const learningEventSchema = z.object({
id: z.string(),
signal: feedbackSignalSchema,
timestamp: z.number(),
taskId: z.string().optional(),
workspacePath: z.string().optional(),
mode: z.string().optional(),
context: z.object({
userTurnCount: z.number().optional(),
toolIterationCount: z.number().optional(),
toolNames: z.array(z.string()).optional(),
promptFingerprint: z.string().optional(),
errorKey: z.string().optional(),
codeIndex: z
.object({
available: z.boolean(),
hits: z.number(),
topScore: z.number().optional(),
})
.optional(),
}),
outcome: z.object({
success: z.boolean().optional(),
corrected: z.boolean().optional(),
summary: z.string().optional(),
confidenceDelta: z.number().optional(),
}),
})

export type LearningEvent = z.infer<typeof learningEventSchema>

/**
* PatternState - lifecycle state for learned patterns
*/
export const patternStateSchema = z.enum(["active", "stale", "archived"])

export type PatternState = z.infer<typeof patternStateSchema>

/**
* PatternType - category of learned pattern
*/
export const patternTypeSchema = z.enum(["prompt", "tool", "error", "skill", "code-index"])

export type PatternType = z.infer<typeof patternTypeSchema>

/**
* LearnedPattern - a pattern extracted from learning events
*/
export const learnedPatternSchema = z.object({
id: z.string(),
patternType: patternTypeSchema,
state: patternStateSchema,
summary: z.string(),
confidenceScore: z.number().min(0).max(1),
frequency: z.number().int().min(0),
successRate: z.number().min(0).max(1),
firstSeenAt: z.number(),
lastSeenAt: z.number(),
lastAppliedAt: z.number().optional(),
sourceSignals: z.array(feedbackSignalSchema),
context: z.object({
toolNames: z.array(z.string()).optional(),
errorKeys: z.array(z.string()).optional(),
modes: z.array(z.string()).optional(),
workspacePaths: z.array(z.string()).optional(),
}),
})

export type LearnedPattern = z.infer<typeof learnedPatternSchema>

/**
* ActionType - types of improvement actions
*/
export const actionTypeSchema = z.enum(["PROMPT_ENRICHMENT", "TOOL_PREFERENCE", "ERROR_AVOIDANCE", "SKILL_SUGGESTION"])

export type ActionType = z.infer<typeof actionTypeSchema>

/**
* ImprovementAction - an action to apply based on learned patterns
*/
export const improvementActionSchema = z.object({
id: z.string(),
actionType: actionTypeSchema,
target: z.enum(["system-prompt", "task-execution", "skills-manager", "review-queue"]),
payload: z.record(z.string(), z.unknown()),
timestamp: z.number(),
})

export type ImprovementAction = z.infer<typeof improvementActionSchema>

/**
* LearningTelemetry - telemetry counters for the learning system
*/
export const learningTelemetrySchema = z.object({
promptEnrichmentUses: z.number().int().default(0),
toolPreferenceUses: z.number().int().default(0),
errorAvoidanceUses: z.number().int().default(0),
skillSuggestionCount: z.number().int().default(0),
lastReviewAt: z.number().optional(),
lastCuratorRunAt: z.number().optional(),
})

export type LearningTelemetry = z.infer<typeof learningTelemetrySchema>

/**
* LearningState - full serializable state of the learning system
*/
export const learningStateSchema = z.object({
version: z.literal(1),
config: learningConfigSchema,
counters: z.object({
userTurnsSinceReview: z.number().int().default(0),
toolIterationsSinceReview: z.number().int().default(0),
}),
patterns: z.array(learnedPatternSchema).default([]),
archivedPatterns: z.array(learnedPatternSchema).default([]),
recentEvents: z.array(learningEventSchema).default([]),
pendingActions: z.array(improvementActionSchema).default([]),
telemetry: learningTelemetrySchema,
})

export type LearningState = z.infer<typeof learningStateSchema>

export const EMPTY_LEARNING_STATE: LearningState = {
version: 1,
config: DEFAULT_LEARNING_CONFIG,
counters: {
userTurnsSinceReview: 0,
toolIterationsSinceReview: 0,
},
patterns: [],
archivedPatterns: [],
recentEvents: [],
pendingActions: [],
telemetry: {
promptEnrichmentUses: 0,
toolPreferenceUses: 0,
errorAvoidanceUses: 0,
skillSuggestionCount: 0,
},
}
29 changes: 29 additions & 0 deletions packages/types/src/memory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from "zod"

/**
* MemoryEntry - a single durable memory entry for prompt-facing context
* Adapted from Hermes' bounded memory store concept.
*/
export const memoryEntrySchema = z.object({
id: z.string(),
content: z.string().max(2000),
source: z.enum(["learning", "user", "system", "review"]),
createdAt: z.number(),
updatedAt: z.number(),
relevanceScore: z.number().min(0).max(1).optional(),
tags: z.array(z.string()).optional(),
expiresAt: z.number().optional(),
})

export type MemoryEntry = z.infer<typeof memoryEntrySchema>

/**
* MemoryContext - bounded set of memory entries for prompt injection
*/
export const memoryContextSchema = z.object({
entries: z.array(memoryEntrySchema).max(10),
revision: z.number().int().default(0),
generatedAt: z.number(),
})

export type MemoryContext = z.infer<typeof memoryContextSchema>
9 changes: 9 additions & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,15 @@ export type ExtensionState = Pick<
hasOpenedModeSelector: boolean
openRouterImageApiKey?: string
messageQueue?: QueuedMessage[]
selfImprovingStatus?: {
enabled: boolean
started: boolean
patternCount: number
eventCount: number
actionCount: number
lastReviewAt?: number
lastCuratorRunAt?: number
}
lastShownAnnouncementId?: string
apiModelId?: string
mcpServers?: McpServer[]
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ vi.mock("../core/webview/ClineProvider", async () => {
postStateToWebview: vi.fn(),
postStateToWebviewWithoutClineMessages: vi.fn(),
getState: vi.fn().mockResolvedValue({}),
initializeSelfImproving: vi.fn().mockResolvedValue(undefined),
initializeCloudProfileSyncWhenReady: vi.fn().mockResolvedValue(undefined),
providerSettingsManager: {},
contextProxy: { getGlobalState: vi.fn() },
Expand Down
31 changes: 31 additions & 0 deletions src/core/prompts/__tests__/system-prompt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { ModeConfig } from "@roo-code/types"

import { SYSTEM_PROMPT } from "../system"
import { McpHub } from "../../../services/mcp/McpHub"
import type { SelfImprovingManager } from "../../../services/self-improving"
import { defaultModeSlug, modes, Mode } from "../../../shared/modes"
import "../../../utils/path"
import { addCustomInstructions } from "../sections/custom-instructions"
Expand Down Expand Up @@ -272,6 +273,36 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-undefined-mcp-hub.snap")
})

it("should include learned guidance before rules when available", async () => {
const selfImprovingManager = {
getPromptContextString: () => "\n## Learned Guidance\n- [prompt] Search relevant code before editing\n",
} as unknown as SelfImprovingManager

const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false,
undefined, // mcpHub
undefined, // diffStrategy
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
experiments,
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // settings
undefined, // todoList
undefined, // modelId
undefined, // skillsManager
selfImprovingManager,
)

expect(prompt).toContain("## Learned Guidance")
expect(prompt).toContain("- [prompt] Search relevant code before editing")
expect(prompt.indexOf("## Learned Guidance")).toBeLessThan(prompt.indexOf("====\n\nRULES"))
})

it("should include vscode language in custom instructions", async () => {
// Mock vscode.env.language
const vscode = vi.mocked(await import("vscode")) as any
Expand Down
Loading