Skip to content

Commit 93f577e

Browse files
authored
Open-ended wish() call creates suggestion.tsx (commontoolsinc#2203)
* Open-ended `wish()` call creates `suggestion.tsx` * Add `wishAndNavigate` tool * Fix intermittent crash in `fetchAndRunPattern` * Fix inconsistency
1 parent bb2b2af commit 93f577e

File tree

8 files changed

+259
-72
lines changed

8 files changed

+259
-72
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/// <cts-enable />
2+
import { NAME, pattern, UI, wish } from "commontools";
3+
4+
export default pattern<Record<string, never>>((_) => {
5+
const wishResult = wish<{ content: string }>({
6+
query: "a nice poem about cats",
7+
});
8+
9+
return {
10+
[NAME]: "Wish tester",
11+
[UI]: (
12+
<div>
13+
<pre>{wishResult.result.content}</pre>
14+
<hr />
15+
{wishResult}
16+
</div>
17+
),
18+
};
19+
});

packages/patterns/common-tools.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,13 @@ export const fetchAndRunPattern = recipe<FetchAndRunPatternInput>(
222222
fetchProgram({ url });
223223

224224
// Use derive to safely handle when program is undefined/pending
225+
// Filter out undefined elements to handle race condition where array proxy
226+
// pre-allocates with undefined before populating elements
225227
const compileParams = derive(program, (p) => ({
226-
files: p?.files ?? [],
228+
files: (p?.files ?? []).filter(
229+
(f): f is { name: string; contents: string } =>
230+
f !== undefined && f !== null && typeof f.name === "string",
231+
),
227232
main: p?.main ?? "",
228233
input: args,
229234
}));

packages/patterns/omnibox-fab.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import {
55
handler,
66
ifElse,
77
NAME,
8+
navigateTo,
89
pattern,
910
patternTool,
1011
UI,
12+
when,
13+
wish,
1114
} from "commontools";
1215
import Chatbot from "./chatbot.tsx";
1316
import {
@@ -42,6 +45,20 @@ const dismissPeek = handler<
4245
peekDismissedIndex.set(assistantMessageCount);
4346
});
4447

48+
/** Wish for a #tag or a custom query with optional linked context. Automatically navigates to the result. */
49+
type WishToolParameters = { query: string; context?: Record<string, any> };
50+
51+
const wishTool = pattern<WishToolParameters>(
52+
({ query, context }) => {
53+
const wishResult = wish<any>({
54+
query,
55+
context,
56+
});
57+
58+
return when(wishResult, navigateTo(wishResult));
59+
},
60+
);
61+
4562
export default pattern<OmniboxFABInput>(
4663
(_) => {
4764
const omnibot = Chatbot({
@@ -60,6 +77,7 @@ export default pattern<OmniboxFABInput>(
6077
fetchAndRunPattern: patternTool(fetchAndRunPattern),
6178
listPatternIndex: patternTool(listPatternIndex),
6279
navigateTo: patternTool(navigateToPattern),
80+
wishAndNavigate: patternTool(wishTool),
6381
},
6482
});
6583

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/// <cts-enable />
2+
import { Cell, Default, derive, NAME, pattern, UI } from "commontools";
3+
import Suggestion from "./suggestion.tsx";
4+
5+
export default pattern<{ title: Default<string, "Suggestion Tester"> }>(
6+
({ title }) => {
7+
const suggestion = Suggestion({
8+
situation: "gimme counter plz",
9+
context: {},
10+
});
11+
12+
const suggestion2 = Suggestion({
13+
situation: "gimme note with the attached content",
14+
context: {
15+
content: "This is the expected content",
16+
value: Cell.of(0),
17+
},
18+
});
19+
20+
return {
21+
[NAME]: title,
22+
[UI]: (
23+
<div>
24+
<h1>Suggestion Tester</h1>
25+
<h2>Counter</h2>
26+
<ct-cell-context $cell={suggestion} label="Counter Suggestion">
27+
{derive(suggestion, (s) => {
28+
return s?.result ?? "waiting...";
29+
})}
30+
</ct-cell-context>
31+
32+
<h2>Note</h2>
33+
<ct-cell-context $cell={suggestion2} label="Note Suggestion">
34+
{derive(suggestion2, (s) => {
35+
return s?.result ?? "waiting...";
36+
})}
37+
</ct-cell-context>
38+
</div>
39+
),
40+
suggestion,
41+
};
42+
},
43+
);

packages/patterns/suggestion.tsx

Lines changed: 25 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,81 +2,40 @@
22
import {
33
Cell,
44
computed,
5-
Default,
65
derive,
76
generateObject,
8-
ifElse,
9-
NAME,
107
pattern,
118
patternTool,
129
toSchema,
1310
UI,
11+
type WishState,
1412
} from "commontools";
1513
import { fetchAndRunPattern, listPatternIndex } from "./common-tools.tsx";
1614

17-
export const Suggestion = pattern(
18-
(
19-
{ situation, context }: {
20-
situation: string;
21-
context: { [id: string]: any };
15+
export default pattern<
16+
{ situation: string; context: { [id: string]: any } },
17+
WishState<Cell<any>>
18+
>(({ situation, context }) => {
19+
const suggestion = generateObject({
20+
system: "Find a useful pattern, run it, pass link to final result",
21+
prompt: situation,
22+
context,
23+
tools: {
24+
fetchAndRunPattern: patternTool(fetchAndRunPattern),
25+
listPatternIndex: patternTool(listPatternIndex),
2226
},
23-
) => {
24-
const suggestion = generateObject({
25-
system: "Find a useful pattern, run it, pass link to final result",
26-
prompt: situation,
27-
context,
28-
tools: {
29-
fetchAndRunPattern: patternTool(fetchAndRunPattern),
30-
listPatternIndex: patternTool(listPatternIndex),
31-
},
32-
model: "anthropic:claude-haiku-4-5",
33-
schema: toSchema<{ cell: Cell<any> }>(),
34-
});
27+
model: "anthropic:claude-haiku-4-5",
28+
schema: toSchema<{ cell: Cell<any> }>(),
29+
});
3530

36-
return ifElse(
37-
computed(() => suggestion.pending && !suggestion.result),
38-
undefined,
39-
suggestion.result,
40-
);
41-
},
42-
);
31+
const result = computed(() => suggestion.result?.cell);
4332

44-
export default pattern<{ title: Default<string, "Suggestion Tester"> }>(
45-
({ title }) => {
46-
const suggestion = Suggestion({
47-
situation: "gimme counter plz",
48-
context: {},
49-
});
50-
51-
const suggestion2 = Suggestion({
52-
situation: "gimme note with the attached content",
53-
context: {
54-
content: "This is the expected content",
55-
value: Cell.of(0),
56-
},
57-
});
58-
59-
return {
60-
[NAME]: title,
61-
[UI]: (
62-
<div>
63-
<h1>Suggestion Tester</h1>
64-
<h2>Counter</h2>
65-
<ct-cell-context $cell={suggestion} label="Counter Suggestion">
66-
{derive(suggestion, (s) => {
67-
return s?.cell ?? "waiting...";
68-
})}
69-
</ct-cell-context>
70-
71-
<h2>Note</h2>
72-
<ct-cell-context $cell={suggestion2} label="Note Suggestion">
73-
{derive(suggestion2, (s) => {
74-
return s?.cell ?? "waiting..";
75-
})}
76-
</ct-cell-context>
77-
</div>
78-
),
79-
suggestion,
80-
};
81-
},
82-
);
33+
return {
34+
result,
35+
[UI]: (
36+
<ct-cell-context $cell={result}>
37+
{derive(result, (r) => r ?? "Searching...")}
38+
</ct-cell-context>
39+
),
40+
};
41+
});

packages/patterns/todo-list.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <cts-enable />
22
import { Cell, Default, derive, NAME, pattern, UI } from "commontools";
3-
import { Suggestion } from "./suggestion.tsx";
3+
import Suggestion from "./suggestion.tsx";
44

55
interface TodoItem {
66
title: string;
@@ -97,7 +97,7 @@ export default pattern<Input, Output>(({ items }) => {
9797
>
9898
<h3>AI Suggestion</h3>
9999
{derive(suggestion, (s) =>
100-
s?.cell ?? (
100+
s?.result ?? (
101101
<span style={{ opacity: 0.6 }}>Getting suggestion...</span>
102102
))}
103103
</div>

packages/runner/src/builtins/fetch-program.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,62 @@ interface FetchCacheEntry {
2727
state: FetchState;
2828
}
2929

30+
// Full schema for cache structure to ensure proper validation when reading back
31+
// from storage. Without this, nested arrays may have undefined elements due to
32+
// incomplete schema-based transformation.
3033
const cacheSchema = {
3134
type: "object",
3235
default: {},
36+
additionalProperties: {
37+
type: "object",
38+
properties: {
39+
inputHash: { type: "string" },
40+
state: {
41+
anyOf: [
42+
{ type: "object", properties: { type: { const: "idle" } } },
43+
{
44+
type: "object",
45+
properties: {
46+
type: { const: "fetching" },
47+
requestId: { type: "string" },
48+
startTime: { type: "number" },
49+
},
50+
},
51+
{
52+
type: "object",
53+
properties: {
54+
type: { const: "success" },
55+
data: {
56+
type: "object",
57+
properties: {
58+
files: {
59+
type: "array",
60+
items: {
61+
type: "object",
62+
properties: {
63+
name: { type: "string" },
64+
contents: { type: "string" },
65+
},
66+
required: ["name", "contents"],
67+
},
68+
},
69+
main: { type: "string" },
70+
},
71+
required: ["files", "main"],
72+
},
73+
},
74+
},
75+
{
76+
type: "object",
77+
properties: {
78+
type: { const: "error" },
79+
message: { type: "string" },
80+
},
81+
},
82+
],
83+
},
84+
},
85+
},
3386
} as const satisfies JSONSchema;
3487

3588
/**

0 commit comments

Comments
 (0)