setup-agent 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/host.ts CHANGED
@@ -34,10 +34,10 @@ const useCheckoutHost = (restored: unknown) => {
34
34
 
35
35
  const requireOpen = () => {
36
36
  if (state.createdAt === null) {
37
- throw reject("not-created", "the checkout has not been created yet");
37
+ throw reject("not-created", "the setup has not been created yet");
38
38
  }
39
39
  if (isClosed(state)) {
40
- throw reject("closed", `the checkout is ${state.status}`);
40
+ throw reject("closed", `the setup is ${state.status}`);
41
41
  }
42
42
  };
43
43
 
@@ -90,7 +90,7 @@ const useCheckoutHost = (restored: unknown) => {
90
90
  if (state.products.some((product) => product.slug === seed.product)) {
91
91
  throw reject(
92
92
  "invalid-input",
93
- `"${seed.product}" is already part of this checkout`,
93
+ `"${seed.product}" is already part of this setup`,
94
94
  );
95
95
  }
96
96
  } else if (kind !== "text" && !seed.options?.length) {
@@ -98,13 +98,36 @@ const useCheckoutHost = (restored: unknown) => {
98
98
  }
99
99
  if (
100
100
  seed.multiple &&
101
- (kind !== "choice" || seed.options?.some((option) => option.variants))
101
+ (kind !== "choice" ||
102
+ seed.options?.some((option) => option.variants?.length))
102
103
  ) {
103
104
  throw reject(
104
105
  "invalid-input",
105
106
  "only a choice whose options have no variants accepts several answers",
106
107
  );
107
108
  }
109
+ const unique = (ids: readonly string[]) => new Set(ids).size === ids.length;
110
+ if (
111
+ !unique(seed.options?.map((option) => option.id) ?? []) ||
112
+ seed.options?.some(
113
+ (option) =>
114
+ !unique(option.variants?.map((variant) => variant.id) ?? []),
115
+ )
116
+ ) {
117
+ throw reject("invalid-input", "option and variant ids must be unique");
118
+ }
119
+ if (
120
+ seed.options?.some(
121
+ (option) =>
122
+ option.id.includes(":") ||
123
+ option.variants?.some((variant) => variant.id.includes(":")),
124
+ )
125
+ ) {
126
+ throw reject(
127
+ "invalid-input",
128
+ 'option and variant ids cannot contain ":", which separates them in an answer',
129
+ );
130
+ }
108
131
  if (
109
132
  seed.default !== undefined &&
110
133
  !seed.options?.some((option) => option.id === seed.default)
@@ -177,7 +200,7 @@ const useCheckoutHost = (restored: unknown) => {
177
200
  const commands = useStatewireCommands<Checkout.Commands>({
178
201
  "checkout/create": ({ id, products, instructions }) => {
179
202
  if (state.id === id) {
180
- throw reject("already-created", "the checkout already exists");
203
+ throw reject("already-created", "the setup already exists");
181
204
  }
182
205
  const fresh = initialCheckoutState();
183
206
  if (!isAgentPresent(state)) state.agent = fresh.agent;
@@ -306,7 +329,7 @@ const useCheckoutHost = (restored: unknown) => {
306
329
  plan.feedback = params.feedback.trim();
307
330
  },
308
331
  "checkout/cancel": () => {
309
- if (state.status === "cancelled") return;
332
+ if (state.createdAt === null || isClosed(state)) return;
310
333
  state.status = "cancelled";
311
334
  dismissOpenInputs();
312
335
  },
@@ -428,7 +451,7 @@ const useCheckoutHost = (restored: unknown) => {
428
451
  };
429
452
  };
430
453
 
431
- /** The checkout statewire host; pass the durable object's restored snapshot. */
454
+ /** The setup statewire host; pass the durable object's restored snapshot. */
432
455
  export const CheckoutHost = (restored?: unknown) =>
433
456
  resource(useCheckoutHost)(restored);
434
457
 
package/src/presets.ts CHANGED
@@ -10,7 +10,7 @@ export type PresetId = "framework" | "llm-provider" | "project";
10
10
  export type PresetOverrides = {
11
11
  /** Restrict the options to these ids, in this order; one id locks the choice in. */
12
12
  only?: readonly string[];
13
- /** Options the agent found itself, listed before the preset's own. */
13
+ /** Options the agent found itself, listed before the preset's own; `only` leaves them out. */
14
14
  found?: readonly { id: string; description?: string }[];
15
15
  default?: string;
16
16
  optional?: boolean;
@@ -139,7 +139,8 @@ export const INPUT_PRESETS: Record<PresetId, Preset> = {
139
139
  },
140
140
  };
141
141
 
142
- export const isPresetId = (id: string): id is PresetId => id in INPUT_PRESETS;
142
+ export const isPresetId = (id: string): id is PresetId =>
143
+ Object.hasOwn(INPUT_PRESETS, id);
143
144
 
144
145
  /** Build the seed for a standard input, throwing on an unknown preset, option or default. */
145
146
  export const presetInput = (
@@ -164,7 +165,7 @@ export const presetInput = (
164
165
  }
165
166
  return { ...option, label: option.id };
166
167
  });
167
- const options = [...found, ...listed];
168
+ const options = overrides.only ? listed : [...found, ...listed];
168
169
  const fallback = options.some((o) => o.id === preset.default)
169
170
  ? preset.default
170
171
  : undefined;
package/src/protocol.ts CHANGED
@@ -282,12 +282,17 @@ export const isClosed = (state: Checkout.State) =>
282
282
  export const finishProposed = (state: Checkout.State) =>
283
283
  !isClosed(state) && state.completion !== undefined;
284
284
 
285
- /** True when the user messaged the agent after it last proposed to close. */
285
+ /** True while a message the user sent after the agent proposed to close still waits for the agent. */
286
286
  export const followedUpSinceProposal = (state: Checkout.State) => {
287
287
  const proposedAt = state.completion?.proposedAt;
288
288
  return (
289
289
  proposedAt !== undefined &&
290
- state.log.some((entry) => entry.role === "user" && entry.at > proposedAt)
290
+ state.log.some(
291
+ (entry) =>
292
+ entry.role === "user" &&
293
+ entry.at > proposedAt &&
294
+ entry.acknowledgedAt === undefined,
295
+ )
291
296
  );
292
297
  };
293
298
 
@@ -376,7 +381,9 @@ export const isOptionIcon = (value: string): value is OptionIcon =>
376
381
  (OPTION_ICONS as readonly string[]).includes(value);
377
382
 
378
383
  export const parseChoiceAnswer = (answer: string) => {
379
- const [option = "", variant] = answer.split(":", 2);
384
+ const parts = answer.split(":");
385
+ if (parts.length > 2) return { option: answer };
386
+ const [option = "", variant] = parts;
380
387
  return { option, ...(variant !== undefined && { variant }) };
381
388
  };
382
389