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
19 changes: 19 additions & 0 deletions packages/patterns/arbitrary-wish-example.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <cts-enable />
import { NAME, pattern, UI, wish } from "commontools";

export default pattern<Record<string, never>>((_) => {
const wishResult = wish<{ content: string }>({
query: "a nice poem about cats",
});

return {
[NAME]: "Wish tester",
[UI]: (
<div>
<pre>{wishResult.result.content}</pre>
<hr />
{wishResult}
</div>
),
};
});
7 changes: 6 additions & 1 deletion packages/patterns/common-tools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,13 @@ export const fetchAndRunPattern = recipe<FetchAndRunPatternInput>(
fetchProgram({ url });

// Use derive to safely handle when program is undefined/pending
// Filter out undefined elements to handle race condition where array proxy
// pre-allocates with undefined before populating elements
const compileParams = derive(program, (p) => ({
files: p?.files ?? [],
files: (p?.files ?? []).filter(
(f): f is { name: string; contents: string } =>
f !== undefined && f !== null && typeof f.name === "string",
),
main: p?.main ?? "",
input: args,
}));
Expand Down
18 changes: 18 additions & 0 deletions packages/patterns/omnibox-fab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@ import {
handler,
ifElse,
NAME,
navigateTo,
pattern,
patternTool,
UI,
when,
wish,
} from "commontools";
import Chatbot from "./chatbot.tsx";
import {
Expand Down Expand Up @@ -42,6 +45,20 @@ const dismissPeek = handler<
peekDismissedIndex.set(assistantMessageCount);
});

/** Wish for a #tag or a custom query with optional linked context. Automatically navigates to the result. */
type WishToolParameters = { query: string; context?: Record<string, any> };

const wishTool = pattern<WishToolParameters>(
({ query, context }) => {
const wishResult = wish<any>({
query,
context,
});

return when(wishResult, navigateTo(wishResult));
},
);

export default pattern<OmniboxFABInput>(
(_) => {
const omnibot = Chatbot({
Expand All @@ -60,6 +77,7 @@ export default pattern<OmniboxFABInput>(
fetchAndRunPattern: patternTool(fetchAndRunPattern),
listPatternIndex: patternTool(listPatternIndex),
navigateTo: patternTool(navigateToPattern),
wishAndNavigate: patternTool(wishTool),
},
});

Expand Down
43 changes: 43 additions & 0 deletions packages/patterns/suggestion-test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/// <cts-enable />
import { Cell, Default, derive, NAME, pattern, UI } from "commontools";
import Suggestion from "./suggestion.tsx";

export default pattern<{ title: Default<string, "Suggestion Tester"> }>(
({ title }) => {
const suggestion = Suggestion({
situation: "gimme counter plz",
context: {},
});

const suggestion2 = Suggestion({
situation: "gimme note with the attached content",
context: {
content: "This is the expected content",
value: Cell.of(0),
},
});

return {
[NAME]: title,
[UI]: (
<div>
<h1>Suggestion Tester</h1>
<h2>Counter</h2>
<ct-cell-context $cell={suggestion} label="Counter Suggestion">
{derive(suggestion, (s) => {
return s?.result ?? "waiting...";
})}
</ct-cell-context>

<h2>Note</h2>
<ct-cell-context $cell={suggestion2} label="Note Suggestion">
{derive(suggestion2, (s) => {
return s?.result ?? "waiting...";
})}
</ct-cell-context>
</div>
),
suggestion,
};
},
);
91 changes: 25 additions & 66 deletions packages/patterns/suggestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,81 +2,40 @@
import {
Cell,
computed,
Default,
derive,
generateObject,
ifElse,
NAME,
pattern,
patternTool,
toSchema,
UI,
type WishState,
} from "commontools";
import { fetchAndRunPattern, listPatternIndex } from "./common-tools.tsx";

export const Suggestion = pattern(
(
{ situation, context }: {
situation: string;
context: { [id: string]: any };
export default pattern<
{ situation: string; context: { [id: string]: any } },
WishState<Cell<any>>
>(({ situation, context }) => {
const suggestion = generateObject({
system: "Find a useful pattern, run it, pass link to final result",
prompt: situation,
context,
tools: {
fetchAndRunPattern: patternTool(fetchAndRunPattern),
listPatternIndex: patternTool(listPatternIndex),
},
) => {
const suggestion = generateObject({
system: "Find a useful pattern, run it, pass link to final result",
prompt: situation,
context,
tools: {
fetchAndRunPattern: patternTool(fetchAndRunPattern),
listPatternIndex: patternTool(listPatternIndex),
},
model: "anthropic:claude-haiku-4-5",
schema: toSchema<{ cell: Cell<any> }>(),
});
model: "anthropic:claude-haiku-4-5",
schema: toSchema<{ cell: Cell<any> }>(),
});

return ifElse(
computed(() => suggestion.pending && !suggestion.result),
undefined,
suggestion.result,
);
},
);
const result = computed(() => suggestion.result?.cell);

export default pattern<{ title: Default<string, "Suggestion Tester"> }>(
({ title }) => {
const suggestion = Suggestion({
situation: "gimme counter plz",
context: {},
});

const suggestion2 = Suggestion({
situation: "gimme note with the attached content",
context: {
content: "This is the expected content",
value: Cell.of(0),
},
});

return {
[NAME]: title,
[UI]: (
<div>
<h1>Suggestion Tester</h1>
<h2>Counter</h2>
<ct-cell-context $cell={suggestion} label="Counter Suggestion">
{derive(suggestion, (s) => {
return s?.cell ?? "waiting...";
})}
</ct-cell-context>

<h2>Note</h2>
<ct-cell-context $cell={suggestion2} label="Note Suggestion">
{derive(suggestion2, (s) => {
return s?.cell ?? "waiting..";
})}
</ct-cell-context>
</div>
),
suggestion,
};
},
);
return {
result,
[UI]: (
<ct-cell-context $cell={result}>
{derive(result, (r) => r ?? "Searching...")}
</ct-cell-context>
),
};
});
4 changes: 2 additions & 2 deletions packages/patterns/todo-list.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <cts-enable />
import { Cell, Default, derive, NAME, pattern, UI } from "commontools";
import { Suggestion } from "./suggestion.tsx";
import Suggestion from "./suggestion.tsx";

interface TodoItem {
title: string;
Expand Down Expand Up @@ -97,7 +97,7 @@ export default pattern<Input, Output>(({ items }) => {
>
<h3>AI Suggestion</h3>
{derive(suggestion, (s) =>
s?.cell ?? (
s?.result ?? (
<span style={{ opacity: 0.6 }}>Getting suggestion...</span>
))}
</div>
Expand Down
53 changes: 53 additions & 0 deletions packages/runner/src/builtins/fetch-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,62 @@ interface FetchCacheEntry {
state: FetchState;
}

// Full schema for cache structure to ensure proper validation when reading back
// from storage. Without this, nested arrays may have undefined elements due to
// incomplete schema-based transformation.
const cacheSchema = {
type: "object",
default: {},
additionalProperties: {
type: "object",
properties: {
inputHash: { type: "string" },
state: {
anyOf: [
{ type: "object", properties: { type: { const: "idle" } } },
{
type: "object",
properties: {
type: { const: "fetching" },
requestId: { type: "string" },
startTime: { type: "number" },
},
},
{
type: "object",
properties: {
type: { const: "success" },
data: {
type: "object",
properties: {
files: {
type: "array",
items: {
type: "object",
properties: {
name: { type: "string" },
contents: { type: "string" },
},
required: ["name", "contents"],
},
},
main: { type: "string" },
},
required: ["files", "main"],
},
},
},
{
type: "object",
properties: {
type: { const: "error" },
message: { type: "string" },
},
},
],
},
},
},
} as const satisfies JSONSchema;

/**
Expand Down
Loading
Loading