Skip to content
Merged
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
26 changes: 25 additions & 1 deletion apps/backend/src/services/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,22 @@ const SandboxDefSchema = Schema.Struct({
}),
});

const MemoryDefSchema = Schema.Struct({
recent_count: Schema.optionalWith(Schema.Number, { default: () => 10 }),
search_candidates: Schema.optionalWith(Schema.Number, {
default: () => 15,
}),
max_relevant: Schema.optionalWith(Schema.Number, { default: () => 3 }),
});

const TomlSchema = Schema.Struct({
default_model: Schema.String,
background_model: Schema.optional(Schema.String),
providers: Schema.Record({
key: Schema.String,
value: ProviderDefSchema,
}),
memory: Schema.optional(MemoryDefSchema),
sandbox: Schema.optional(SandboxDefSchema),
});

Expand Down Expand Up @@ -106,9 +115,16 @@ export type SandboxConfig = {
readonly dependencies: ReadonlyArray<SandboxDependencyConfig>;
};

export type MemoryConfig = {
readonly recentCount: number;
readonly searchCandidates: number;
readonly maxRelevant: number;
};

export type AppConfig = {
readonly models: ModelsConfig;
readonly sandbox: Option.Option<SandboxConfig>;
readonly memory: MemoryConfig;
};

// Convention: provider "groq" → env var "GROQ_API_KEY"
Expand Down Expand Up @@ -209,6 +225,13 @@ export const loadAppConfig = Effect.gen(function* () {
? yield* resolveSandbox(toml.sandbox)
: Option.none<SandboxConfig>();

// Resolve memory config (optional — defaults apply when absent)
const memory: MemoryConfig = {
recentCount: toml.memory?.recent_count ?? 10,
searchCandidates: toml.memory?.search_candidates ?? 15,
maxRelevant: toml.memory?.max_relevant ?? 3,
};

yield* Effect.log("Config loaded", {
providers: providers.map((p) => p.name),
models: providers.flatMap((p) => p.models.map((m) => m.id)),
Expand All @@ -225,7 +248,8 @@ export const loadAppConfig = Effect.gen(function* () {
deps: s.dependencies.map((d) => d.package),
}),
}),
memory,
});

return { models, sandbox } satisfies AppConfig;
return { models, sandbox, memory } satisfies AppConfig;
});
2 changes: 1 addition & 1 deletion apps/backend/src/services/generate/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const MAX_RETRY_ATTEMPTS = 3;

// Streaming system prompt - compact but complete
export const STREAMING_PATCH_PROMPT = `You are cuttlekit, a generative UI engine.
Users describe what they want and you build it as live HTML. You also handle user actions like button clicks, form inputs, and selections to update the UI accordingly.
Build live HTML from user descriptions. Handle actions (clicks, inputs, selections) to update UI.

OUTPUT: JSONL, one JSON per line with "op" field. Stream multiple small lines, NOT one big line.
{"op":"patches","patches":[...]} - 1-3 patches per line MAX, try under 400 chars each. Many changes = many lines, one item per line.
Expand Down
30 changes: 16 additions & 14 deletions apps/backend/src/services/generate/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { MemoryService, type MemorySearchResult } from "../memory/index.js";
import { accumulateLinesWithFlush } from "../../stream/utils.js";
import { PatchValidator, renderCETree, getCompactHtmlFromCtx, type Patch, type ValidationContext } from "../vdom/index.js";
import { ModelRegistry } from "../model-registry.js";
import { loadAppConfig } from "../app-config.js";
import {
PatchSchema,
LLMResponseSchema,
Expand Down Expand Up @@ -35,6 +36,7 @@ export class GenerateService extends Effect.Service<GenerateService>()(
const memory = yield* MemoryService;
const patchValidator = yield* PatchValidator;
const toolService = yield* ToolService;
const { memory: memoryConfig } = yield* loadAppConfig;

// ============================================================
// Parse JSON line - fails with JsonParseError for retry
Expand Down Expand Up @@ -386,14 +388,14 @@ export class GenerateService extends Effect.Service<GenerateService>()(
// Fetch recent entries and semantic search results
const [recentEntries, relevantEntries] = yield* Effect.all([
memory
.getRecent(sessionId, 5)
.getRecent(sessionId, memoryConfig.recentCount)
.pipe(
Effect.catchAll(() =>
Effect.succeed([] as MemorySearchResult[]),
),
),
memory
.search(sessionId, searchQuery, 8)
.search(sessionId, searchQuery, memoryConfig.searchCandidates)
.pipe(
Effect.catchAll(() =>
Effect.succeed([] as MemorySearchResult[]),
Expand All @@ -405,10 +407,20 @@ export class GenerateService extends Effect.Service<GenerateService>()(
const recentIds = new Set(recentEntries.map((e) => e.id));
const uniqueRelevant = relevantEntries
.filter((e) => !recentIds.has(e.id))
.slice(0, 3);
.slice(0, memoryConfig.maxRelevant);

// Build history (context only, not to act on)
// Build history: relevant context first (background), then recent (timeline closest to [NOW])
const historyParts: string[] = [];
if (uniqueRelevant.length > 0) {
historyParts.push(
`[RELEVANT PAST CONTEXT]\n${uniqueRelevant
.map(
(e) =>
`- ${e.promptSummary ? `"${e.promptSummary}" → ` : ""}${e.changeSummary}`,
)
.join("\n")}`,
);
}
if (recentEntries.length > 0) {
historyParts.push(
`[RECENT CHANGES]\n${recentEntries
Expand All @@ -421,16 +433,6 @@ export class GenerateService extends Effect.Service<GenerateService>()(
.join("\n")}`,
);
}
if (uniqueRelevant.length > 0) {
historyParts.push(
`[RELEVANT PAST CONTEXT]\n${uniqueRelevant
.map(
(e) =>
`- ${e.promptSummary ? `"${e.promptSummary}" → ` : ""}${e.changeSummary}`,
)
.join("\n")}`,
);
}

// Build current actions (most volatile - goes at end)
// Lists all batched actions/prompts in chronological order (1 = oldest, N = latest)
Expand Down
20 changes: 13 additions & 7 deletions apps/backend/src/services/memory/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,19 @@ const MemorySummariesSchema = z.object({
promptSummary: z
.string()
.nullable()
.describe("Summary of user prompts in one sentence, or null if no prompts"),
.describe(
"Compressed user intent, 3-8 words. Drop articles/filler. Null if no prompts.",
),
actionSummary: z
.string()
.nullable()
.describe("Summary of user actions in one sentence, or null if no actions"),
.describe(
"Compressed action, 3-8 words. Drop articles/filler. Null if no actions.",
),
changeSummary: z
.string()
.describe(
"1-2 sentence summary of what changed visually/functionally, not technical details",
"Compressed visual/functional change, 5-12 words. Drop articles/connectives/filler. Keep facts/numbers.",
),
});

Expand Down Expand Up @@ -143,7 +147,9 @@ export class MemoryService extends Effect.Service<MemoryService>()(
? `User actions:\n${op.actions.map((a, i) => `${i + 1}. ${a.action}${a.data ? ` (data: ${JSON.stringify(a.data)})` : ""}`).join("\n")}`
: "No user actions.";

const prompt = `Analyze these UI changes and generate summaries.
const prompt = `Analyze UI changes. Generate compressed summaries.

COMPRESSION: Drop articles, connectives, filler. Active voice, present tense. Keep facts/numbers/specifics.

${promptContext}

Expand All @@ -152,9 +158,9 @@ ${actionContext}
${changeDescription}

Generate:
1. promptSummary: If there are prompts, summarize them in one sentence. Otherwise null.
2. actionSummary: If there are actions, summarize them in one sentence. Otherwise null.
3. changeSummary: Describe what changed visually/functionally in 1-2 sentences.`;
1. promptSummary: Compressed user intent (3-8 words). Null if no prompts.
2. actionSummary: Compressed action (3-8 words). Null if no actions.
3. changeSummary: Compressed visual/functional change (5-12 words).`;

const result = yield* Effect.promise(() =>
generateText({
Expand Down
Loading