@condition-sh/core 2026.9.2 → 2026.9.3

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/index.js CHANGED
@@ -98,6 +98,74 @@ function validateInput(schema, value) {
98
98
  check(schema, value, "input", errors);
99
99
  return errors;
100
100
  }
101
+
102
+ // packages/core/src/traits.ts
103
+ var isList = (values) => Array.isArray(values);
104
+ function describeValues(values) {
105
+ return isList(values) ? values.join(", ") : `an integer from ${values.minimum} to ${values.maximum}`;
106
+ }
107
+ function accepts(values, value) {
108
+ if (isList(values))
109
+ return values.includes(value);
110
+ return Number.isInteger(value) && value >= values.minimum && value <= values.maximum;
111
+ }
112
+ function requirementProblems(specs, resolved) {
113
+ const problems = [];
114
+ for (const [name, value] of Object.entries(resolved)) {
115
+ for (const [other, allowed] of Object.entries(specs[name].requires?.[String(value)] ?? {})) {
116
+ if (allowed.includes(resolved[other]))
117
+ continue;
118
+ const actual = other in resolved ? `, not "${resolved[other]}"` : "";
119
+ problems.push(`${name} "${value}" needs ${other} to be one of ${allowed.join(", ")}${actual}`);
120
+ }
121
+ }
122
+ return problems;
123
+ }
124
+ function resolveTraits(specs, requested = {}) {
125
+ if (requested === null || typeof requested !== "object" || Array.isArray(requested))
126
+ throw new Error("traits must be an object");
127
+ const asked = requested;
128
+ const available = specs ?? {};
129
+ const problems = [];
130
+ const resolved = {};
131
+ for (const name of Object.keys(asked)) {
132
+ if (!(name in available))
133
+ problems.push(`unknown trait "${name}"; choose from ${Object.keys(available).join(", ") || "none"}`);
134
+ }
135
+ for (const [name, spec] of Object.entries(available)) {
136
+ const value = name in asked ? asked[name] : spec.default;
137
+ if (value === undefined)
138
+ continue;
139
+ if (accepts(spec.values, value))
140
+ resolved[name] = value;
141
+ else
142
+ problems.push(`${name} can't be ${JSON.stringify(value)}; choose ${describeValues(spec.values)}`);
143
+ }
144
+ if (!problems.length)
145
+ problems.push(...requirementProblems(available, resolved));
146
+ if (problems.length)
147
+ throw new Error(`invalid traits: ${problems.join("; ")}`);
148
+ return resolved;
149
+ }
150
+ function summarizeTraits(specs) {
151
+ return Object.fromEntries(Object.entries(specs).map(([name, { description, values, default: fallback, requires }]) => [name, {
152
+ ...description ? { description } : {},
153
+ values,
154
+ ...fallback === undefined ? {} : { default: fallback },
155
+ ...requires ? { requires } : {}
156
+ }]));
157
+ }
158
+ async function buildState(prepare, specs, context) {
159
+ let output = await prepare(context);
160
+ for (const [name, spec] of Object.entries(specs ?? {})) {
161
+ if (!(name in context.traits))
162
+ continue;
163
+ const next = await spec.apply({ ...context, output, value: context.traits[name] });
164
+ if (next !== undefined)
165
+ output = next;
166
+ }
167
+ return output;
168
+ }
101
169
  // packages/core/src/remote-inbox.ts
102
170
  import { createHash } from "node:crypto";
103
171
  var actorPattern = /^[a-z][a-z0-9-]{0,15}$/;
@@ -203,7 +271,8 @@ function describeProject(project) {
203
271
  name,
204
272
  description: scenario.description,
205
273
  ...scenario.input ? { input: scenario.input } : {},
206
- ...scenario.actors ? { actors: scenario.actors } : {}
274
+ ...scenario.actors ? { actors: scenario.actors } : {},
275
+ ...scenario.traits ? { traits: summarizeTraits(scenario.traits) } : {}
207
276
  }))
208
277
  };
209
278
  }
@@ -213,6 +282,7 @@ function publicRun(run, project) {
213
282
  project,
214
283
  scenario: run.scenario,
215
284
  input: parse(run.presentedInput),
285
+ traits: JSON.parse(run.traits),
216
286
  output: parse(run.presentedOutput),
217
287
  status: run.status,
218
288
  leaseUntil: run.leaseUntil,
@@ -273,9 +343,17 @@ class Condition {
273
343
  }
274
344
  async start(name, input = {}, options = {}) {
275
345
  const scenario = this.scenario(name);
276
- const problems = scenario.input ? validateInput(scenario.input, input) : [];
346
+ const inputProblems = scenario.input ? validateInput(scenario.input, input) : [];
347
+ const problems = inputProblems.length ? [`invalid input: ${inputProblems.join("; ")}`] : [];
348
+ let traits = {};
349
+ try {
350
+ traits = resolveTraits(scenario.traits, options.traits);
351
+ } catch (error) {
352
+ problems.push(message(error));
353
+ }
277
354
  if (problems.length)
278
- throw new Error(`invalid input: ${problems.join("; ")}`);
355
+ throw new Error(problems.join("; "));
356
+ const traitsJson = JSON.stringify(traits);
279
357
  const wantsInbox = !scenario.actors || scenario.actors.some((actor) => actor.inbox);
280
358
  const minutes = options.leaseMinutes ?? 20;
281
359
  if (!Number.isInteger(minutes) || minutes < 1 || minutes > 1440)
@@ -290,6 +368,7 @@ class Condition {
290
368
  id: crypto.randomUUID(),
291
369
  scenario: name,
292
370
  input: inputJson,
371
+ traits: traitsJson,
293
372
  output: null,
294
373
  status: "preparing",
295
374
  leaseUntil: new Date(now.getTime() + minutes * 60000).toISOString(),
@@ -306,7 +385,7 @@ class Condition {
306
385
  const existing = run.requestKey ? await this.store.findByRequestKey(run.requestKey) : null;
307
386
  if (!existing)
308
387
  throw new Error("run conflict");
309
- if (existing.scenario !== name || existing.input !== inputJson)
388
+ if (existing.scenario !== name || existing.input !== inputJson || existing.traits !== traitsJson)
310
389
  throw new Error("requestKey was used with different input");
311
390
  return { run: this.publicRun(existing), created: false };
312
391
  }
@@ -316,6 +395,7 @@ class Condition {
316
395
  throw new Error(`run ${id} is ${run.status}`);
317
396
  const scenario = this.scenario(run.scenario);
318
397
  const input = JSON.parse(run.input);
398
+ const traits = JSON.parse(run.traits);
319
399
  const inbox = this.project.inbox;
320
400
  let step = "prepare";
321
401
  try {
@@ -329,7 +409,7 @@ class Condition {
329
409
  await inbox.reserve({ runId: id, project: this.project.name, leaseUntil: run.leaseUntil });
330
410
  step = "prepare";
331
411
  }
332
- const output = await scenario.prepare({ runId: id, input, inbox: this.inboxFor(run) });
412
+ const output = await buildState(scenario.prepare, scenario.traits, { runId: id, input, traits, inbox: this.inboxFor(run) });
333
413
  const saved = await this.store.update(id, "preparing", {
334
414
  output: JSON.stringify(output ?? null),
335
415
  presentedOutput: await presentation(scenario.present, output ?? null)
@@ -337,7 +417,7 @@ class Condition {
337
417
  if (!saved)
338
418
  throw new Error(`run ${id} changed during preparation`);
339
419
  step = "verify";
340
- await scenario.verify({ runId: id, input, output, inbox: this.inboxFor(run) });
420
+ await scenario.verify({ runId: id, input, traits, output, inbox: this.inboxFor(run) });
341
421
  if (!await this.store.update(id, "preparing", { status: "ready" }))
342
422
  throw new Error(`run ${id} changed during verification`);
343
423
  } catch (error) {
@@ -360,7 +440,7 @@ class Condition {
360
440
  if (!scenario.login)
361
441
  throw new Error(`scenario ${run.scenario} does not provide login`);
362
442
  this.checkActor(run, actor, "login");
363
- return scenario.login({ runId: id, input: JSON.parse(run.input), output: parse(run.output), actor, inbox: this.inboxFor(run) });
443
+ return scenario.login({ runId: id, input: JSON.parse(run.input), traits: JSON.parse(run.traits), output: parse(run.output), actor, inbox: this.inboxFor(run) });
364
444
  }
365
445
  async inbox(id, actor) {
366
446
  const run = await this.read(id);
@@ -394,7 +474,7 @@ class Condition {
394
474
  throw new Error(`run ${id} is ${run.status}`);
395
475
  const scenario = this.scenario(run.scenario);
396
476
  try {
397
- await scenario.cleanup({ runId: id, input: JSON.parse(run.input), output: parse(run.output), inbox: this.inboxFor(run) });
477
+ await scenario.cleanup({ runId: id, input: JSON.parse(run.input), traits: JSON.parse(run.traits), output: parse(run.output), inbox: this.inboxFor(run) });
398
478
  if (run.inboxRef) {
399
479
  if (!this.project.inbox || this.project.inbox.identity !== run.inboxRef) {
400
480
  throw new Error(`inbox configuration for run ${id} is unavailable or changed`);
@@ -432,6 +512,8 @@ class Condition {
432
512
  }
433
513
  export {
434
514
  validateInput,
515
+ summarizeTraits,
516
+ resolveTraits,
435
517
  publicRun,
436
518
  loadProject,
437
519
  legacyInboxDomain,
@@ -439,6 +521,7 @@ export {
439
521
  inboxAddress,
440
522
  describeProject,
441
523
  defineProject,
524
+ buildState,
442
525
  RemoteInbox,
443
526
  Condition
444
527
  };
package/local.js CHANGED
@@ -98,6 +98,74 @@ function validateInput(schema, value) {
98
98
  check(schema, value, "input", errors);
99
99
  return errors;
100
100
  }
101
+
102
+ // packages/core/src/traits.ts
103
+ var isList = (values) => Array.isArray(values);
104
+ function describeValues(values) {
105
+ return isList(values) ? values.join(", ") : `an integer from ${values.minimum} to ${values.maximum}`;
106
+ }
107
+ function accepts(values, value) {
108
+ if (isList(values))
109
+ return values.includes(value);
110
+ return Number.isInteger(value) && value >= values.minimum && value <= values.maximum;
111
+ }
112
+ function requirementProblems(specs, resolved) {
113
+ const problems = [];
114
+ for (const [name, value] of Object.entries(resolved)) {
115
+ for (const [other, allowed] of Object.entries(specs[name].requires?.[String(value)] ?? {})) {
116
+ if (allowed.includes(resolved[other]))
117
+ continue;
118
+ const actual = other in resolved ? `, not "${resolved[other]}"` : "";
119
+ problems.push(`${name} "${value}" needs ${other} to be one of ${allowed.join(", ")}${actual}`);
120
+ }
121
+ }
122
+ return problems;
123
+ }
124
+ function resolveTraits(specs, requested = {}) {
125
+ if (requested === null || typeof requested !== "object" || Array.isArray(requested))
126
+ throw new Error("traits must be an object");
127
+ const asked = requested;
128
+ const available = specs ?? {};
129
+ const problems = [];
130
+ const resolved = {};
131
+ for (const name of Object.keys(asked)) {
132
+ if (!(name in available))
133
+ problems.push(`unknown trait "${name}"; choose from ${Object.keys(available).join(", ") || "none"}`);
134
+ }
135
+ for (const [name, spec] of Object.entries(available)) {
136
+ const value = name in asked ? asked[name] : spec.default;
137
+ if (value === undefined)
138
+ continue;
139
+ if (accepts(spec.values, value))
140
+ resolved[name] = value;
141
+ else
142
+ problems.push(`${name} can't be ${JSON.stringify(value)}; choose ${describeValues(spec.values)}`);
143
+ }
144
+ if (!problems.length)
145
+ problems.push(...requirementProblems(available, resolved));
146
+ if (problems.length)
147
+ throw new Error(`invalid traits: ${problems.join("; ")}`);
148
+ return resolved;
149
+ }
150
+ function summarizeTraits(specs) {
151
+ return Object.fromEntries(Object.entries(specs).map(([name, { description, values, default: fallback, requires }]) => [name, {
152
+ ...description ? { description } : {},
153
+ values,
154
+ ...fallback === undefined ? {} : { default: fallback },
155
+ ...requires ? { requires } : {}
156
+ }]));
157
+ }
158
+ async function buildState(prepare, specs, context) {
159
+ let output = await prepare(context);
160
+ for (const [name, spec] of Object.entries(specs ?? {})) {
161
+ if (!(name in context.traits))
162
+ continue;
163
+ const next = await spec.apply({ ...context, output, value: context.traits[name] });
164
+ if (next !== undefined)
165
+ output = next;
166
+ }
167
+ return output;
168
+ }
101
169
  // packages/core/src/remote-inbox.ts
102
170
  import { createHash } from "node:crypto";
103
171
  var actorPattern = /^[a-z][a-z0-9-]{0,15}$/;
@@ -203,7 +271,8 @@ function describeProject(project) {
203
271
  name,
204
272
  description: scenario.description,
205
273
  ...scenario.input ? { input: scenario.input } : {},
206
- ...scenario.actors ? { actors: scenario.actors } : {}
274
+ ...scenario.actors ? { actors: scenario.actors } : {},
275
+ ...scenario.traits ? { traits: summarizeTraits(scenario.traits) } : {}
207
276
  }))
208
277
  };
209
278
  }
@@ -213,6 +282,7 @@ function publicRun(run, project) {
213
282
  project,
214
283
  scenario: run.scenario,
215
284
  input: parse(run.presentedInput),
285
+ traits: JSON.parse(run.traits),
216
286
  output: parse(run.presentedOutput),
217
287
  status: run.status,
218
288
  leaseUntil: run.leaseUntil,
@@ -273,9 +343,17 @@ class Condition {
273
343
  }
274
344
  async start(name, input = {}, options = {}) {
275
345
  const scenario = this.scenario(name);
276
- const problems = scenario.input ? validateInput(scenario.input, input) : [];
346
+ const inputProblems = scenario.input ? validateInput(scenario.input, input) : [];
347
+ const problems = inputProblems.length ? [`invalid input: ${inputProblems.join("; ")}`] : [];
348
+ let traits = {};
349
+ try {
350
+ traits = resolveTraits(scenario.traits, options.traits);
351
+ } catch (error) {
352
+ problems.push(message(error));
353
+ }
277
354
  if (problems.length)
278
- throw new Error(`invalid input: ${problems.join("; ")}`);
355
+ throw new Error(problems.join("; "));
356
+ const traitsJson = JSON.stringify(traits);
279
357
  const wantsInbox = !scenario.actors || scenario.actors.some((actor) => actor.inbox);
280
358
  const minutes = options.leaseMinutes ?? 20;
281
359
  if (!Number.isInteger(minutes) || minutes < 1 || minutes > 1440)
@@ -290,6 +368,7 @@ class Condition {
290
368
  id: crypto.randomUUID(),
291
369
  scenario: name,
292
370
  input: inputJson,
371
+ traits: traitsJson,
293
372
  output: null,
294
373
  status: "preparing",
295
374
  leaseUntil: new Date(now.getTime() + minutes * 60000).toISOString(),
@@ -306,7 +385,7 @@ class Condition {
306
385
  const existing = run.requestKey ? await this.store.findByRequestKey(run.requestKey) : null;
307
386
  if (!existing)
308
387
  throw new Error("run conflict");
309
- if (existing.scenario !== name || existing.input !== inputJson)
388
+ if (existing.scenario !== name || existing.input !== inputJson || existing.traits !== traitsJson)
310
389
  throw new Error("requestKey was used with different input");
311
390
  return { run: this.publicRun(existing), created: false };
312
391
  }
@@ -316,6 +395,7 @@ class Condition {
316
395
  throw new Error(`run ${id} is ${run.status}`);
317
396
  const scenario = this.scenario(run.scenario);
318
397
  const input = JSON.parse(run.input);
398
+ const traits = JSON.parse(run.traits);
319
399
  const inbox = this.project.inbox;
320
400
  let step = "prepare";
321
401
  try {
@@ -329,7 +409,7 @@ class Condition {
329
409
  await inbox.reserve({ runId: id, project: this.project.name, leaseUntil: run.leaseUntil });
330
410
  step = "prepare";
331
411
  }
332
- const output = await scenario.prepare({ runId: id, input, inbox: this.inboxFor(run) });
412
+ const output = await buildState(scenario.prepare, scenario.traits, { runId: id, input, traits, inbox: this.inboxFor(run) });
333
413
  const saved = await this.store.update(id, "preparing", {
334
414
  output: JSON.stringify(output ?? null),
335
415
  presentedOutput: await presentation(scenario.present, output ?? null)
@@ -337,7 +417,7 @@ class Condition {
337
417
  if (!saved)
338
418
  throw new Error(`run ${id} changed during preparation`);
339
419
  step = "verify";
340
- await scenario.verify({ runId: id, input, output, inbox: this.inboxFor(run) });
420
+ await scenario.verify({ runId: id, input, traits, output, inbox: this.inboxFor(run) });
341
421
  if (!await this.store.update(id, "preparing", { status: "ready" }))
342
422
  throw new Error(`run ${id} changed during verification`);
343
423
  } catch (error) {
@@ -360,7 +440,7 @@ class Condition {
360
440
  if (!scenario.login)
361
441
  throw new Error(`scenario ${run.scenario} does not provide login`);
362
442
  this.checkActor(run, actor, "login");
363
- return scenario.login({ runId: id, input: JSON.parse(run.input), output: parse(run.output), actor, inbox: this.inboxFor(run) });
443
+ return scenario.login({ runId: id, input: JSON.parse(run.input), traits: JSON.parse(run.traits), output: parse(run.output), actor, inbox: this.inboxFor(run) });
364
444
  }
365
445
  async inbox(id, actor) {
366
446
  const run = await this.read(id);
@@ -394,7 +474,7 @@ class Condition {
394
474
  throw new Error(`run ${id} is ${run.status}`);
395
475
  const scenario = this.scenario(run.scenario);
396
476
  try {
397
- await scenario.cleanup({ runId: id, input: JSON.parse(run.input), output: parse(run.output), inbox: this.inboxFor(run) });
477
+ await scenario.cleanup({ runId: id, input: JSON.parse(run.input), traits: JSON.parse(run.traits), output: parse(run.output), inbox: this.inboxFor(run) });
398
478
  if (run.inboxRef) {
399
479
  if (!this.project.inbox || this.project.inbox.identity !== run.inboxRef) {
400
480
  throw new Error(`inbox configuration for run ${id} is unavailable or changed`);
@@ -448,6 +528,7 @@ function fromRow(row) {
448
528
  id: row.id,
449
529
  scenario: row.scenario,
450
530
  input: row.input,
531
+ traits: row.traits,
451
532
  output: row.output,
452
533
  status: row.status,
453
534
  leaseUntil: row.lease_until,
@@ -476,7 +557,7 @@ class SqliteRunStore {
476
557
  input TEXT NOT NULL, output TEXT, status TEXT NOT NULL,
477
558
  lease_until TEXT NOT NULL, created_at TEXT NOT NULL,
478
559
  error TEXT, request_key TEXT, inbox_ref TEXT,
479
- presented_input TEXT, presented_output TEXT, error_code TEXT,
560
+ presented_input TEXT, presented_output TEXT, error_code TEXT, traits TEXT NOT NULL DEFAULT '{}',
480
561
  UNIQUE(project, request_key)
481
562
  )`);
482
563
  const existing = new Set(this.db.query("PRAGMA table_info(runs)").all().map((column) => column.name));
@@ -484,12 +565,14 @@ class SqliteRunStore {
484
565
  if (!existing.has(column))
485
566
  this.db.exec(`ALTER TABLE runs ADD COLUMN ${column} TEXT`);
486
567
  }
568
+ if (!existing.has("traits"))
569
+ this.db.exec("ALTER TABLE runs ADD COLUMN traits TEXT NOT NULL DEFAULT '{}'");
487
570
  this.db.exec("CREATE INDEX IF NOT EXISTS runs_lease_idx ON runs(status, lease_until)");
488
571
  }
489
572
  async insert(run) {
490
- const inserted = this.db.query(`INSERT INTO runs (id, project, scenario, input, output, status, lease_until, created_at, error, error_code, request_key, inbox_ref, presented_input, presented_output)
491
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
492
- ON CONFLICT(project, request_key) DO NOTHING RETURNING id`).get(run.id, this.project, run.scenario, run.input, run.output, run.status, run.leaseUntil, run.createdAt, run.error, run.errorCode, run.requestKey, run.inboxRef, run.presentedInput, run.presentedOutput);
573
+ const inserted = this.db.query(`INSERT INTO runs (id, project, scenario, input, traits, output, status, lease_until, created_at, error, error_code, request_key, inbox_ref, presented_input, presented_output)
574
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
575
+ ON CONFLICT(project, request_key) DO NOTHING RETURNING id`).get(run.id, this.project, run.scenario, run.input, run.traits, run.output, run.status, run.leaseUntil, run.createdAt, run.error, run.errorCode, run.requestKey, run.inboxRef, run.presentedInput, run.presentedOutput);
493
576
  return inserted !== null;
494
577
  }
495
578
  async find(id) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@condition-sh/core",
3
- "version": "2026.9.2",
3
+ "version": "2026.9.3",
4
4
  "description": "The Condition scenario contract and run engine.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/types/index.d.ts CHANGED
@@ -1,15 +1,19 @@
1
1
  import type { RunStatus, RunStore, StoredRun } from "./store.js";
2
2
  import type { InboxAdapter } from "./remote-inbox.js";
3
3
  import { type JsonSchema } from "./schema.js";
4
+ import { type TraitSpec, type TraitSummary, type Traits } from "./traits.js";
4
5
  export type { ReadMessage } from "./extract.js";
5
6
  export { validateInput } from "./schema.js";
6
7
  export type { JsonSchema } from "./schema.js";
8
+ export { resolveTraits, summarizeTraits, buildState } from "./traits.js";
9
+ export type { TraitSpec, TraitSummary, TraitValue, TraitValues, Traits } from "./traits.js";
7
10
  export { RemoteInbox, inboxAddress, inboxAlias, legacyInboxDomain } from "./remote-inbox.js";
8
11
  export type { InboxAdapter, InboxMessage } from "./remote-inbox.js";
9
12
  export type { RunPatch, RunStatus, RunStore, StoredRun } from "./store.js";
10
13
  export type ScenarioContext<Input> = {
11
14
  runId: string;
12
15
  input: Input;
16
+ traits: Traits;
13
17
  inbox?: {
14
18
  address: (actor: string) => string;
15
19
  };
@@ -23,6 +27,9 @@ export type Scenario<Input = unknown, Output = unknown> = {
23
27
  description: string;
24
28
  input?: JsonSchema;
25
29
  actors?: ActorSpec[];
30
+ traits?: Record<string, TraitSpec<ScenarioContext<Input> & {
31
+ output: Output;
32
+ }>>;
26
33
  prepare: (context: ScenarioContext<Input>) => Promise<Output>;
27
34
  verify: (context: ScenarioContext<Input> & {
28
35
  output: Output;
@@ -49,6 +56,7 @@ export type Run = {
49
56
  project: string;
50
57
  scenario: string;
51
58
  input: unknown;
59
+ traits: Traits;
52
60
  output: unknown | null;
53
61
  status: RunStatus;
54
62
  leaseUntil: string;
@@ -61,11 +69,17 @@ export type ErrorCode = "inbox_unavailable" | "prepare_failed" | "verify_failed"
61
69
  export type ConditionOptions = {
62
70
  now?: () => Date;
63
71
  };
72
+ export type StartOptions = {
73
+ leaseMinutes?: number;
74
+ requestKey?: string;
75
+ traits?: Record<string, unknown>;
76
+ };
64
77
  export type ScenarioSummary = {
65
78
  name: string;
66
79
  description: string;
67
80
  input?: JsonSchema;
68
81
  actors?: ActorSpec[];
82
+ traits?: Record<string, TraitSummary>;
69
83
  };
70
84
  export declare function describeProject(project: Project): {
71
85
  project: string;
@@ -89,18 +103,12 @@ export declare class Condition {
89
103
  private inboxFor;
90
104
  private checkActor;
91
105
  private active;
92
- start(name: string, input?: unknown, options?: {
93
- leaseMinutes?: number;
94
- requestKey?: string;
95
- }): Promise<{
106
+ start(name: string, input?: unknown, options?: StartOptions): Promise<{
96
107
  run: Run;
97
108
  created: boolean;
98
109
  }>;
99
110
  prepare(id: string): Promise<Run>;
100
- condition(name: string, input?: unknown, options?: {
101
- leaseMinutes?: number;
102
- requestKey?: string;
103
- }): Promise<Run>;
111
+ condition(name: string, input?: unknown, options?: StartOptions): Promise<Run>;
104
112
  login(id: string, actor: string): Promise<unknown>;
105
113
  inbox(id: string, actor: string): Promise<{
106
114
  messages: import("./extract").ReadMessage[];
package/types/store.d.ts CHANGED
@@ -3,6 +3,7 @@ export type StoredRun = {
3
3
  id: string;
4
4
  scenario: string;
5
5
  input: string;
6
+ traits: string;
6
7
  output: string | null;
7
8
  status: RunStatus;
8
9
  leaseUntil: string;
@@ -0,0 +1,25 @@
1
+ export type TraitValue = string | number;
2
+ export type Traits = Record<string, TraitValue>;
3
+ export type TraitValues = readonly string[] | {
4
+ type: "integer";
5
+ minimum: number;
6
+ maximum: number;
7
+ };
8
+ export type TraitSummary = {
9
+ description?: string;
10
+ values: TraitValues;
11
+ default?: TraitValue;
12
+ requires?: Record<string, Record<string, readonly TraitValue[]>>;
13
+ };
14
+ export type TraitSpec<Context> = TraitSummary & {
15
+ apply: (context: Context & {
16
+ value: TraitValue;
17
+ }) => Promise<unknown>;
18
+ };
19
+ export declare function resolveTraits(specs: Record<string, TraitSummary> | undefined, requested?: unknown): Traits;
20
+ export declare function summarizeTraits(specs: Record<string, TraitSummary>): Record<string, TraitSummary>;
21
+ export declare function buildState<Context extends {
22
+ traits: Traits;
23
+ }>(prepare: (context: Context) => Promise<unknown>, specs: Record<string, TraitSpec<Context & {
24
+ output: unknown;
25
+ }>> | undefined, context: Context): Promise<unknown>;