Skip to content

Commit b90adf0

Browse files
authored
refactor(charm): remove built-in Charm schema; require explicit schemas (#1819)
* add <T> to isStream to override type * remove [UI] from charm type + first group of fixes based on that. * refactor(charm): remove built-in Charm schema; require explicit schemas - Replace Charm type and charmSchema with schema-driven access. - CharmManager.get now accepts optional schema and returns typed cells. - Export nameSchema and uiSchema; update callers to read NAME and UI via schema. - Introduce generic CharmController<T> and CharmsController<T>. - Background worker: access bgUpdater as Stream via schema; map typed. - Runner: handle Stream in Cellify<T> for asSchema. - CLI, UI, and tests: replace Cell<Charm> with Cell<unknown>; pass schemas or cast. - Persistent flows return typed cells using recipe.resultSchema. - search.ts and format.ts use nameSchema for NAME access. - AppView reads title via cc().get(id, nameSchema) and cell.key(NAME). Motivation: - Decouple CharmManager from a fixed schema; allow arbitrary charm shapes. - Make invalid states unrepresentable by requiring explicit schemas at access. - Enable streams to be typed via schema (asStream) consistently. * ugly fix for ct-kanban: use any instead of unknown. really this should get a proper schema * fix UI usage * call get with runIt true, to keep current behavior * fix formatting * remove ct-kanban for now * addressing various minor PR feedback * fix formatting * extract mentionable schema and use in ct-outliner as well
1 parent 051173d commit b90adf0

File tree

29 files changed

+331
-1864
lines changed

29 files changed

+331
-1864
lines changed

packages/background-charm-service/src/worker.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type Charm, CharmManager } from "@commontools/charm";
1+
import { CharmManager } from "@commontools/charm";
22
import {
33
Cell,
44
type ConsoleHandler,
@@ -7,6 +7,7 @@ import {
77
type ErrorWithContext,
88
isStream,
99
Runtime,
10+
Stream,
1011
} from "@commontools/runner";
1112
import { StorageManager } from "@commontools/runner/storage/cache.deno";
1213

@@ -29,7 +30,7 @@ let latestError: Error | null = null;
2930
let currentSession: Session | null = null;
3031
let manager: CharmManager | null = null;
3132
let runtime: Runtime | null = null;
32-
const loadedCharms = new Map<string, Cell<Charm>>();
33+
const loadedCharms = new Map<string, Cell<{ bgUpdater: Stream<unknown> }>>();
3334

3435
// Error handler that will be passed to Runtime
3536
const errorHandler: ErrorHandler = (e: ErrorWithContext) => {
@@ -163,7 +164,11 @@ async function runCharm(data: RunData): Promise<void> {
163164
if (!runningCharm) {
164165
// If not loaded yet, get it from the manager
165166
console.log(`Loading charm ${charmId} for the first time`);
166-
runningCharm = await manager.get(charmsEntryCell, true);
167+
runningCharm = await manager.get(charmsEntryCell, true, {
168+
type: "object",
169+
properties: { bgUpdater: { asStream: true } },
170+
required: ["bgUpdater"],
171+
});
167172

168173
if (!runningCharm) {
169174
throw new Error(`Charm not found: ${charmId}`);
@@ -176,7 +181,7 @@ async function runCharm(data: RunData): Promise<void> {
176181
}
177182

178183
// Find the updater stream
179-
const updater = runningCharm.key("bgUpdater");
184+
const updater = runningCharm.key("bgUpdater") as Stream<unknown>;
180185
if (!updater || !isStream(updater)) {
181186
throw new Error(`No updater stream found for charm: ${charmId}`);
182187
}

packages/charm/src/commands.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { getIframeRecipe } from "./iframe/recipe.ts";
66
import { extractUserCode, injectUserCode } from "./iframe/static.ts";
77
import { WorkflowForm } from "./index.ts";
88
import { compileAndRunRecipe, generateNewRecipeVersion } from "./iterate.ts";
9-
import { Charm, CharmManager } from "./manager.ts";
9+
import { CharmManager, nameSchema } from "./manager.ts";
1010
import { processWorkflow, ProcessWorkflowOptions } from "./workflow.ts";
1111

1212
export const castSpellAsCharm = async (
@@ -71,10 +71,10 @@ export const createDataCharm = (
7171

7272
export async function fixItCharm(
7373
charmManager: CharmManager,
74-
charm: Cell<Charm>,
74+
charm: Cell<unknown>,
7575
error: Error,
7676
model = DEFAULT_MODEL_NAME,
77-
): Promise<Cell<Charm>> {
77+
): Promise<Cell<unknown>> {
7878
const iframeRecipe = getIframeRecipe(charm, charmManager.runtime);
7979
if (!iframeRecipe.iframe) {
8080
throw new Error("Fixit only works for iframe charms");
@@ -112,8 +112,7 @@ export async function renameCharm(
112112
charmId: string,
113113
newName: string,
114114
): Promise<void> {
115-
const charm = await charmManager.get(charmId);
116-
if (!charm) return;
115+
const charm = await charmManager.get(charmId, false, nameSchema);
117116
charm.key(NAME).set(newName);
118117
}
119118

@@ -122,7 +121,7 @@ export async function addGithubRecipe(
122121
filename: string,
123122
spec: string,
124123
runOptions: unknown,
125-
): Promise<Cell<Charm>> {
124+
): Promise<Cell<unknown>> {
126125
const response = await fetch(
127126
`https://raw.githubusercontent.com/commontoolsinc/labs/refs/heads/main/recipes/${filename}?${Date.now()}`,
128127
);
@@ -151,10 +150,10 @@ export async function addGithubRecipe(
151150
export async function modifyCharm(
152151
charmManager: CharmManager,
153152
promptText: string,
154-
currentCharm: Cell<Charm>,
153+
currentCharm: Cell<unknown>,
155154
prefill?: Partial<WorkflowForm>,
156155
model?: string,
157-
): Promise<Cell<Charm>> {
156+
): Promise<Cell<unknown>> {
158157
// Include the current charm in the context
159158
const context: ProcessWorkflowOptions = {
160159
existingCharm: currentCharm,

packages/charm/src/format.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Module, NAME, Recipe } from "@commontools/runner";
2-
import { CharmManager, charmSchema } from "./index.ts";
2+
import { CharmManager, nameSchema } from "./manager.ts";
33
import { Cell } from "@commontools/runner";
44

55
/**
@@ -84,7 +84,7 @@ export function getCharmNameAsCamelCase(
8484
cell: Cell<unknown>,
8585
usedKeys: Record<string, unknown>,
8686
): string {
87-
const charmName = toCamelCase(cell.asSchema(charmSchema).key(NAME).get());
87+
const charmName = toCamelCase(cell.asSchema(nameSchema).key(NAME).get());
8888

8989
let name = charmName;
9090
let num = 0;

packages/charm/src/iframe/recipe.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Cell, type JSONSchema, type Runtime } from "@commontools/runner";
2-
import { Charm, getRecipeIdFromCharm } from "../manager.ts";
2+
import { getRecipeIdFromCharm } from "../manager.ts";
33

44
export type IFrameRecipe = {
55
src: string;
@@ -62,7 +62,7 @@ function parseIframeRecipe(source: string): IFrameRecipe {
6262
}
6363

6464
export const getIframeRecipe = (
65-
charm: Cell<Charm>,
65+
charm: Cell<unknown>,
6666
runtime: Runtime,
6767
): {
6868
recipeId: string;

packages/charm/src/imagine.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Cell } from "@commontools/runner";
2-
import { Charm } from "./manager.ts";
32

43
// Re-export workflow types and functions from workflow module
54
export type { WorkflowConfig, WorkflowType } from "./workflow.ts";
@@ -13,13 +12,13 @@ export interface ParsedMention {
1312
originalText: string;
1413
startIndex: number;
1514
endIndex: number;
16-
charm: Cell<Charm>;
15+
charm: Cell<unknown>;
1716
}
1817

1918
/**
2019
* Result of processing a prompt with mentions
2120
*/
2221
export interface ProcessedPrompt {
2322
text: string; // Processed text with mentions replaced by readable names
24-
mentions: Record<string, Cell<Charm>>; // Map of mention IDs to charm cells
23+
mentions: Record<string, Cell<unknown>>; // Map of mention IDs to charm cells
2524
}

packages/charm/src/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
export {
2-
type Charm,
32
charmId,
43
charmListSchema,
54
CharmManager,
6-
charmSchema,
75
getRecipeIdFromCharm,
6+
type NameSchema,
7+
nameSchema,
88
processSchema,
9+
type UISchema,
10+
uiSchema,
911
} from "./manager.ts";
1012
export { searchCharms } from "./search.ts";
1113
export {

packages/charm/src/iterate.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
type Runtime,
1212
type RuntimeProgram,
1313
} from "@commontools/runner";
14-
import { Charm, CharmManager, charmSourceCellSchema } from "./manager.ts";
14+
import { CharmManager, charmSourceCellSchema } from "./manager.ts";
1515
import { buildFullRecipe, getIframeRecipe } from "./iframe/recipe.ts";
1616
import { buildPrompt, RESPONSE_PREFILL } from "./iframe/prompt.ts";
1717
import {
@@ -105,10 +105,10 @@ export const genSrc = async (
105105
*/
106106
export async function iterate(
107107
charmManager: CharmManager,
108-
charm: Cell<Charm>,
108+
charm: Cell<unknown>,
109109
plan: WorkflowForm["plan"],
110110
options?: GenerationOptions,
111-
): Promise<{ cell: Cell<Charm>; llmRequestId?: string }> {
111+
): Promise<{ cell: Cell<unknown>; llmRequestId?: string }> {
112112
const optionsWithDefaults = applyDefaults(options);
113113
const { model, cache, space, generationId } = optionsWithDefaults;
114114
const { iframe } = getIframeRecipe(charm, charmManager.runtime);
@@ -151,7 +151,7 @@ export function extractTitle(src: string, defaultTitle: string): string {
151151

152152
export const generateNewRecipeVersion = async (
153153
charmManager: CharmManager,
154-
parent: Cell<Charm>,
154+
parent: Cell<unknown>,
155155
newRecipe:
156156
& Pick<IFrameRecipe, "src" | "spec">
157157
& Partial<Omit<IFrameRecipe, "src" | "spec">>,
@@ -505,7 +505,7 @@ async function twoPhaseCodeGeneration(
505505
export async function castNewRecipe(
506506
charmManager: CharmManager,
507507
form: WorkflowForm,
508-
): Promise<{ cell: Cell<Charm>; llmRequestId?: string }> {
508+
): Promise<{ cell: Cell<unknown>; llmRequestId?: string }> {
509509
console.log("Processing form:", form);
510510

511511
// Remove $UI, $NAME, and any streams from the cells
@@ -584,7 +584,7 @@ export async function compileAndRunRecipe(
584584
runOptions: unknown,
585585
parents?: string[],
586586
llmRequestId?: string,
587-
): Promise<Cell<Charm>> {
587+
): Promise<Cell<unknown>> {
588588
const recipe = await compileRecipe(
589589
recipeSrc,
590590
spec,

0 commit comments

Comments
 (0)