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
2 changes: 1 addition & 1 deletion apps/hub/src/artifacts/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const ToolRead = ArtifactRead.merge({ "part?": "number.integer > 0" });
const readDefinition = {
name: "read_artifact",
description:
"See which project documents exist, or read one.\nWith no arguments it lists: each document's artifactId, kind, title, current version, sha256, approvedVersion, the version a person approved, and openQuestionCount, the questions its current version leaves open for the person; inFlight, the writer, reviewer, coder and designer runs still open, each with its status, deployed until its first assignment arrives and running after, and each working (mid-turn) or waiting (parked for your next mail), with progress once it has acted: its turns so far, its latest tool calls as tool and target, the files it has written, and lastActivityAt; failed, the latest runs that stopped on an error, with when and why, which deliver nothing more; awaitingApproval, the kinds of filed documents whose current version waits on the person's approval; and preferenceGuidance, how the person has said they want to work.\nWith a kind or an artifactId it returns that document's content at its current version, or at the given version, with the openQuestions that version leaves for the person; a long document comes in numbered parts, and part reads the next one.",
"See which project documents exist, or read one.\nWith no arguments it lists: each document's artifactId, kind, title, current version, sha256, approvedVersion, the version a person approved, and openQuestionCount, the questions its current version leaves open for the person; inFlight, the writer, reviewer, coder and designer runs still open, each with its status, deployed until its first assignment arrives and running after, and each working (mid-turn) or waiting (parked for your next mail), with progress once it has acted: its turns so far, its latest tool calls as tool and target, the files it has written, and lastActivityAt; failed, the latest runs that stopped on an error, with when and why, which deliver nothing more; awaitingApproval, the kinds of filed documents whose current version waits on the person's approval; and preferenceGuidance, how the person said they wanted to work when the project started; what they say in the conversation wins over it.\nWith a kind or an artifactId it returns that document's content at its current version, or at the given version, with the openQuestions that version leaves for the person; a long document comes in numbered parts, and part reads the next one.",
inputSchema: { ...ToolRead.toJsonSchema() },
};
const briefWriteDefinition = {
Expand Down
29 changes: 14 additions & 15 deletions apps/hub/src/config/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,30 +106,34 @@ export const PREFERENCES: readonly PreferenceEntry[] = [
hint: "How many different looks the designers make when you explore several.",
topic: "Design concepts",
options: [
{ id: "one", label: "One", guidance: "Start one design concept." },
{
id: "one",
label: "One",
guidance: "Start one design concept, unless the person asks for more.",
},
{
id: "two",
label: "Two",
guidance:
"When the person wants to explore several looks, start two different design concepts.",
"When the person wants to explore several looks and names no number, start two different design concepts.",
},
{
id: "three",
label: "Three",
guidance:
"When the person wants to explore several looks, start three different design concepts.",
"When the person wants to explore several looks and names no number, start three different design concepts.",
},
{
id: "four",
label: "Four",
guidance:
"When the person wants to explore several looks, start four different design concepts.",
"When the person wants to explore several looks and names no number, start four different design concepts.",
},
{
id: "five",
label: "Five",
guidance:
"When the person wants to explore several looks, start five different design concepts.",
"When the person wants to explore several looks and names no number, start five different design concepts.",
},
],
default: "three",
Expand Down Expand Up @@ -349,19 +353,14 @@ export function preferenceGuidance(
);
}

/** How many design concepts the designers make at once; the options are listed in count order. */
export function designConceptCount(choices: Preferences): number {
/** The most design concepts at work at once: the largest count the preference offers. What the
* person asks for in the conversation decides how many start, up to this, so the preference is
* guidance for the coordinator rather than a limit here. */
export function designConceptCeiling(): number {
const entry = PREFERENCES.find(
(candidate) => candidate.id === "design_concepts",
)!;
const index = entry.options.findIndex(
(option) => option.id === choices[entry.id],
);
return (
(index < 0
? entry.options.findIndex((option) => option.id === entry.default)
: index) + 1
);
return entry.options.length;
}

/** Whether the project's coder adds new packages without a person's approval. */
Expand Down
34 changes: 18 additions & 16 deletions apps/hub/src/projects/deploy-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ test("the coder spawns only from the approved current build plan version", async
}
});

test("design_app starts one designer per concept within the person's limit, each named before its assignment", async () => {
test("design_app starts one designer per concept the person asks for, each named before its assignment", async () => {
const events: string[] = [];
const sha = (n: number) => String(n).repeat(64);
const artifacts = [
Expand All @@ -136,6 +136,7 @@ test("design_app starts one designer per concept within the person's limit, each
sha256: sha(1),
}));
let runs = 0;
let inFlight: Record<string, unknown>[] = [];
const fetch = async (path: string, init: RequestInit) => {
const body = init.body ? JSON.parse(String(init.body)) : undefined;
if (path.endsWith("/workflows/deployments")) {
Expand All @@ -155,7 +156,7 @@ test("design_app starts one designer per concept within the person's limit, each
if (path.endsWith("/artifacts") && init.method === "GET")
return Response.json({
artifacts,
inFlight: [],
inFlight,
preferences: { design_concepts: "two" },
});
return Response.json({ code: "unexpected_route" }, { status: 404 });
Expand Down Expand Up @@ -188,31 +189,32 @@ test("design_app starts one designer per concept within the person's limit, each
const instruction =
"Design the dashboard the requirements describe, in the person's words.";
const lookIdeas = "Calm and readable, like a paper notebook.";
const concepts = ["Sleek", "Pastel", "Retro"].map((label) => ({
label,
direction: label + " look",
}));
const concept = (label: string) => ({ label, direction: label + " look" });
const concepts = ["Sleek", "Pastel", "Retro"].map(concept);

// Only the ceiling on concepts at work holds; the person's two is guidance for the coordinator.
inFlight = ["Calm", "Bold", "Editorial"].map((label, index) => ({
runId: "live-" + String(index),
role: "designer",
status: "running",
concept: label,
}));
expect(
(await run("design_app", { instruction, lookIdeas, concepts })).content,
).toStartWith("too_many_concepts:");
).toStartWith("too_many_concepts: at most 5");
inFlight = [];
// The person asked for three looks while their preference says two: what they say wins.
expect(
(
await run("design_app", {
instruction,
lookIdeas,
concepts: concepts.slice(0, 2),
})
).isError,
(await run("design_app", { instruction, lookIdeas, concepts })).isError,
).toBe(undefined);
// Each concept is recorded against its run before the run's assignment fires it.
for (const [index, label] of ["Sleek", "Pastel"].entries()) {
for (const [index, label] of ["Sleek", "Pastel", "Retro"].entries()) {
const runId = "run-" + String(index + 1);
expect(events.indexOf("concept " + runId + " " + label)).toBeLessThan(
events.indexOf("mail " + label),
);
}
expect(events.filter((event) => event === "deploy")).toHaveLength(2);
expect(events.filter((event) => event === "deploy")).toHaveLength(3);
});

function writerTools(preferences: Record<string, string> = {}) {
Expand Down
14 changes: 8 additions & 6 deletions apps/hub/src/projects/deploy-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
import {
agentDecidesApprovals,
asksOpenQuestions,
designConceptCount,
designConceptCeiling,
preferenceBlock,
} from "../config/preferences.ts";
import {
Expand Down Expand Up @@ -291,7 +291,7 @@ export function createDeployTools(options?: {
{
name: "design_app",
description:
"Have the designer make or change the app's design from the filed problem brief, constraints, approaches and product requirements: a clickable, responsive mockup with a DESIGN.md of its tokens, screens, states and flows. The coder builds only from a filed design. instruction says what to design or change; a designer already working takes it in the same design. lookIdeas carries the person's own ideas for the look in their words, or that they left the look to you, and every designer works from it. The designer starts once a person approves the instruction. Each design arrives later as a message.\n\nWhen the person wants one suggested design, give one concept: the look you suggest, shaped by their ideas. When they want to explore several, give a concept for each look, up to the number of design concepts the person asked for. One designer per concept makes the design in that concept's look and files it as that concept's design_option for the person to compare; a designer already working on a concept takes the instruction in the same option. Choose concepts that differ clearly from one another, from this catalog or of your own that fit the idea better. Each catalog concept starts from a design theme (" +
"Have the designer make or change the app's design from the filed problem brief, constraints, approaches and product requirements: a clickable, responsive mockup with a DESIGN.md of its tokens, screens, states and flows. The coder builds only from a filed design. instruction says what to design or change; a designer already working takes it in the same design. lookIdeas carries the person's own ideas for the look in their words, or that they left the look to you, and every designer works from it. The designer starts once a person approves the instruction. Each design arrives later as a message.\n\nWhen the person wants one suggested design, give one concept: the look you suggest, shaped by their ideas. When they want to explore several, give a concept for each look: as many as the person asked for, or the number their Design concepts preference names when they said no number. What they say in the conversation wins over that preference. At most five concepts can be at work at a time, counting designers already started. One designer per concept makes the design in that concept's look and files it as that concept's design_option for the person to compare; a designer already working on a concept takes the instruction in the same option. Choose concepts that differ clearly from one another, from this catalog or of your own that fit the idea better. Each catalog concept starts from a design theme (" +
DESIGN_THEMES.map((theme) => theme.label).join(", ") +
") and its direction says how its designer adjusts that theme's tokens; a concept of your own starts from the kit's neutral theme, so its direction names the colours, type and radii it wants. When the person chose a theme, design that one concept, with the changes they asked for in its direction:\n" +
DESIGN_CONCEPTS.map(
Expand Down Expand Up @@ -730,8 +730,10 @@ export function createDeployTools(options?: {
result = { runId: live.runId, role: "coder" };
} else if (call.name === "design_app") {
const input = validArguments(DesignApp, call.arguments);
const { inFlight, preferences } = await listed();
const limit = designConceptCount(preferences);
const { inFlight } = await listed();
// The person decides how many concepts start; the preference guides the coordinator when
// they named no number, so only the ceiling holds here.
const limit = designConceptCeiling();
// A requested concept already at work takes the instruction rather than adding a designer.
const concepts = new Set(
inFlight.flatMap((run) =>
Expand All @@ -741,9 +743,9 @@ export function createDeployTools(options?: {
for (const concept of input.concepts) concepts.add(concept.label);
if (concepts.size > limit)
throw new Refusal(
"too_many_concepts: the person asked for at most " +
"too_many_concepts: at most " +
String(limit) +
" design concepts at a time",
" design concepts can be at work at a time, counting those already started",
);
// Each concept has its own long-lived designer.
const design = (concept: DesignConcept) =>
Expand Down
6 changes: 4 additions & 2 deletions docs/USER_MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,10 @@ the **Design concepts to compare** it will make.
- **Start the design** lets it begin.
- **Not yet** stops it so you can say what to change.

If you asked to explore, you get as many concepts as **Design concepts to
compare** says (three by default).
If you asked to explore, you get as many concepts as you asked for, up to five
at a time. If you did not say how many, you get as many as **Design concepts to
compare** says (three by default). What you say in the conversation wins over
that setting.

### Comparing concepts

Expand Down