@wibly/internal-manifest 0.1.1 → 0.1.2
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/CHANGELOG.md +8 -0
- package/package.json +3 -3
- package/src/fixtures.ts +18 -10
- package/src/index.ts +19 -4
- package/src/manifest.test.ts +8 -4
- package/src/manifest.ts +40 -35
- package/src/phase.test.ts +37 -7
- package/src/phase.ts +29 -37
- package/src/session-settings.test.ts +50 -0
- package/src/session-settings.ts +190 -0
- package/src/validate.test.ts +30 -1
- package/src/validate.ts +60 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# `@wibly/internal-manifest` — Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 — 2026-06-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `workflow.abortPhaseId` (optional) — the phase a `host.abort` jumps
|
|
8
|
+
to. When omitted the Runtime aborts to the last declared phase.
|
|
9
|
+
Validated against the declared phase ids (`unknown_abort_phase`).
|
|
10
|
+
|
|
3
11
|
## 0.1.1 — 2026-05-30
|
|
4
12
|
|
|
5
13
|
Initial public npm release. Internal runtime dependency of `@wibly/sdk` and
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wibly/internal-manifest",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Wibly @wibly/internal-manifest",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@wibly/internal-protocol": "0.1.
|
|
21
|
-
"@wibly/internal-shared": "0.1.
|
|
20
|
+
"@wibly/internal-protocol": "0.1.2",
|
|
21
|
+
"@wibly/internal-shared": "0.1.2",
|
|
22
22
|
"zod": "^3.25.76"
|
|
23
23
|
},
|
|
24
24
|
"peerDependencies": {}
|
package/src/fixtures.ts
CHANGED
|
@@ -146,18 +146,22 @@ export const validManifestFixture: Manifest = {
|
|
|
146
146
|
],
|
|
147
147
|
promptSlots: {
|
|
148
148
|
experienceSystem: 'You are running Hello World, a tiny test Experience.',
|
|
149
|
-
|
|
150
|
-
host_open_phase:
|
|
149
|
+
callTemplates: {
|
|
150
|
+
host_open_phase: {
|
|
151
|
+
prompt: 'Open the next phase with one short sentence.',
|
|
152
|
+
qualityTier: 'standard',
|
|
153
|
+
fallbackResponse: 'Welcome — let us begin.',
|
|
154
|
+
},
|
|
151
155
|
host_recap: {
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
prompt: {
|
|
157
|
+
template: 'Recap the round for {playerCount} players.',
|
|
158
|
+
vars: ['playerCount'],
|
|
159
|
+
},
|
|
160
|
+
qualityTier: 'standard',
|
|
161
|
+
fallbackResponse: 'Thanks for playing.',
|
|
154
162
|
},
|
|
155
163
|
},
|
|
156
164
|
},
|
|
157
|
-
fallbackResponses: {
|
|
158
|
-
host_open_phase: 'Welcome — let us begin.',
|
|
159
|
-
host_recap: 'Thanks for playing.',
|
|
160
|
-
},
|
|
161
165
|
widgetDependencies: ['Lobby', 'GuessInput', 'Leaderboard'],
|
|
162
166
|
contentRating: {
|
|
163
167
|
tier: 'none',
|
|
@@ -205,12 +209,16 @@ export const cloneFixture = (): Manifest =>
|
|
|
205
209
|
*/
|
|
206
210
|
export const cloneFixtureWithInferenceSideEffect = (): Manifest => {
|
|
207
211
|
const fixture = cloneFixture();
|
|
212
|
+
fixture.promptSlots.callTemplates.host_judge = {
|
|
213
|
+
prompt: 'Judge submissions.',
|
|
214
|
+
qualityTier: 'standard',
|
|
215
|
+
fallbackResponse: 'tie',
|
|
216
|
+
};
|
|
208
217
|
patchPhase(fixture, 'guess', {
|
|
209
218
|
sideEffects: [
|
|
210
219
|
{
|
|
211
220
|
kind: 'inference',
|
|
212
|
-
|
|
213
|
-
qualityTier: 'standard',
|
|
221
|
+
templateId: 'host_judge',
|
|
214
222
|
targetPath: '/session/inference/host_judge',
|
|
215
223
|
},
|
|
216
224
|
],
|
package/src/index.ts
CHANGED
|
@@ -51,13 +51,12 @@ export {
|
|
|
51
51
|
AwardCriterionSchema,
|
|
52
52
|
AwardDefinitionSchema,
|
|
53
53
|
BundleContextSlotsSchema,
|
|
54
|
-
|
|
54
|
+
CallTemplateSchema,
|
|
55
55
|
ConcurrentOpportunitySchema,
|
|
56
56
|
ContentRatingAudienceSchema,
|
|
57
57
|
ContentRatingFloorSchema,
|
|
58
58
|
ContentRatingSchema,
|
|
59
59
|
ExperienceContentRatingTierSchema,
|
|
60
|
-
FallbackResponsesSchema,
|
|
61
60
|
GameplayImageSchema,
|
|
62
61
|
GameplayVideoSchema,
|
|
63
62
|
InferenceEnvelopeSchema,
|
|
@@ -74,6 +73,9 @@ export {
|
|
|
74
73
|
PromptSlotsSchema,
|
|
75
74
|
PromptSlotValueSchema,
|
|
76
75
|
QualityTierSchema,
|
|
76
|
+
PLATFORM_CLASSIFY_TEMPLATE_ID,
|
|
77
|
+
getCallTemplate,
|
|
78
|
+
isPlatformClassifyTemplate,
|
|
77
79
|
ScoringAggregatorSchema,
|
|
78
80
|
ScoringDimensionSchema,
|
|
79
81
|
ScoringSchema,
|
|
@@ -82,13 +84,12 @@ export {
|
|
|
82
84
|
type AwardCriterion,
|
|
83
85
|
type AwardDefinition,
|
|
84
86
|
type BundleContextSlots,
|
|
85
|
-
type
|
|
87
|
+
type CallTemplate,
|
|
86
88
|
type ConcurrentOpportunity,
|
|
87
89
|
type ContentRating,
|
|
88
90
|
type ContentRatingAudience,
|
|
89
91
|
type ContentRatingFloor,
|
|
90
92
|
type ExperienceContentRatingTier,
|
|
91
|
-
type FallbackResponses,
|
|
92
93
|
type GameplayImage,
|
|
93
94
|
type GameplayVideo,
|
|
94
95
|
type InferenceEnvelope,
|
|
@@ -109,10 +110,24 @@ export {
|
|
|
109
110
|
type Scoring,
|
|
110
111
|
type ScoringAggregator,
|
|
111
112
|
type ScoringDimension,
|
|
113
|
+
type SessionSettings,
|
|
112
114
|
type StateSchema,
|
|
113
115
|
type Workflow,
|
|
114
116
|
} from './manifest.js';
|
|
115
117
|
|
|
118
|
+
export {
|
|
119
|
+
resolveSessionSettings,
|
|
120
|
+
SessionSettingBooleanDefinitionSchema,
|
|
121
|
+
SessionSettingDefinitionSchema,
|
|
122
|
+
SessionSettingEnumDefinitionSchema,
|
|
123
|
+
SessionSettingNumberDefinitionSchema,
|
|
124
|
+
SessionSettingsSchema,
|
|
125
|
+
validateSessionSettingsOverrides,
|
|
126
|
+
type ResolvedSessionSettings,
|
|
127
|
+
type SessionSettingDefinition,
|
|
128
|
+
type SessionSettingsValidationResult,
|
|
129
|
+
} from './session-settings.js';
|
|
130
|
+
|
|
116
131
|
export {
|
|
117
132
|
validateManifest,
|
|
118
133
|
type ValidationError,
|
package/src/manifest.test.ts
CHANGED
|
@@ -138,13 +138,18 @@ describe('PromptSlotValueSchema / PromptSlotsSchema', () => {
|
|
|
138
138
|
).toBe(false);
|
|
139
139
|
});
|
|
140
140
|
|
|
141
|
-
it('
|
|
141
|
+
it('accepts callTemplates with free-form ids', () => {
|
|
142
142
|
expect(
|
|
143
143
|
PromptSlotsSchema.safeParse({
|
|
144
144
|
experienceSystem: 'sys',
|
|
145
|
-
|
|
145
|
+
callTemplates: {
|
|
146
|
+
my_custom_template: {
|
|
147
|
+
prompt: 'hello',
|
|
148
|
+
qualityTier: 'standard',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
146
151
|
}).success,
|
|
147
|
-
).toBe(
|
|
152
|
+
).toBe(true);
|
|
148
153
|
});
|
|
149
154
|
});
|
|
150
155
|
|
|
@@ -266,7 +271,6 @@ describe('ManifestSchema — required fields', () => {
|
|
|
266
271
|
'scoring',
|
|
267
272
|
'lifecyclePolicies',
|
|
268
273
|
'promptSlots',
|
|
269
|
-
'fallbackResponses',
|
|
270
274
|
'widgetDependencies',
|
|
271
275
|
'contentRating',
|
|
272
276
|
'portalMetadata',
|
package/src/manifest.ts
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
* Session:
|
|
8
8
|
*
|
|
9
9
|
* - identity & ownership (`id`, `version`, `tenant`, `creator`),
|
|
10
|
-
* - safety / cost shape (`inferenceEnvelope`, `contentRating
|
|
11
|
-
* `fallbackResponses`),
|
|
10
|
+
* - safety / cost shape (`inferenceEnvelope`, `contentRating`),
|
|
12
11
|
* - workflow (`workflow.phases`, `concurrentOpportunities`,
|
|
13
12
|
* `lifecyclePolicies`),
|
|
14
13
|
* - state shape (`stateSchema`),
|
|
@@ -32,13 +31,17 @@
|
|
|
32
31
|
import { z } from 'zod';
|
|
33
32
|
|
|
34
33
|
import {
|
|
35
|
-
CallKindSchema,
|
|
36
34
|
CollectionRuleSchema,
|
|
37
35
|
InputSetSchema,
|
|
38
36
|
PhaseSchema,
|
|
39
37
|
} from './phase.js';
|
|
38
|
+
import { SessionSettingsSchema } from './session-settings.js';
|
|
40
39
|
|
|
41
|
-
|
|
40
|
+
/** Platform-internal template id for the safety classifier (not in manifest). */
|
|
41
|
+
export const PLATFORM_CLASSIFY_TEMPLATE_ID = 'classify';
|
|
42
|
+
|
|
43
|
+
export const isPlatformClassifyTemplate = (templateId: string): boolean =>
|
|
44
|
+
templateId === PLATFORM_CLASSIFY_TEMPLATE_ID;
|
|
42
45
|
|
|
43
46
|
// -----------------------------------------------------------------------------
|
|
44
47
|
// JSON value (used for state-schema slices and scoring payloads)
|
|
@@ -194,6 +197,14 @@ export const StateSchemaSchema = z.object({
|
|
|
194
197
|
export const WorkflowSchema = z.object({
|
|
195
198
|
initialPhase: z.string().min(1),
|
|
196
199
|
phases: z.array(PhaseSchema).min(1),
|
|
200
|
+
/**
|
|
201
|
+
* Phase the platform jumps to on `host.abort` (a universal mid-game
|
|
202
|
+
* abort, handled by the Runtime — games need NO per-phase abort wiring).
|
|
203
|
+
* Optional: when omitted, the Runtime aborts to the LAST declared phase
|
|
204
|
+
* (conventionally the final/results screen). Set this only to override
|
|
205
|
+
* that default.
|
|
206
|
+
*/
|
|
207
|
+
abortPhaseId: z.string().min(1).optional(),
|
|
197
208
|
});
|
|
198
209
|
|
|
199
210
|
// -----------------------------------------------------------------------------
|
|
@@ -360,43 +371,27 @@ export const PromptSlotValueSchema = z.union([
|
|
|
360
371
|
]);
|
|
361
372
|
|
|
362
373
|
/**
|
|
363
|
-
*
|
|
364
|
-
* the
|
|
365
|
-
*
|
|
366
|
-
* here MUST appear in that document; new callKinds land in the chunk
|
|
367
|
-
* that introduces them with the doc updated in the same commit.
|
|
368
|
-
*
|
|
369
|
-
* Defined in `phase.ts` (re-exported above) so the chunk-B8c
|
|
370
|
-
* `WorkflowSideEffect.inference.callKind` schema can reference it
|
|
371
|
-
* without a circular import.
|
|
374
|
+
* One creator-defined LLM call template. Game code invokes by
|
|
375
|
+
* `templateId` (the record key); Studio edits prompt, schema, tier,
|
|
376
|
+
* and fallback copy without redeploying bundles.
|
|
372
377
|
*/
|
|
378
|
+
export const CallTemplateSchema = z.object({
|
|
379
|
+
prompt: PromptSlotValueSchema,
|
|
380
|
+
returnSchema: JsonValueSchema.optional(),
|
|
381
|
+
qualityTier: QualityTierSchema.default('standard'),
|
|
382
|
+
fallbackResponse: z.string().min(1).optional(),
|
|
383
|
+
});
|
|
373
384
|
|
|
374
385
|
/**
|
|
375
386
|
* Manifest-supplied prompt content. `experienceSystem` is the layer-3
|
|
376
|
-
* Experience system message; `
|
|
377
|
-
*
|
|
378
|
-
* the Gateway renders. Per the prompt-composition doc, the platform
|
|
379
|
-
* supplies the structural layers (1, 4-structural, 5, 6, 8) and the
|
|
380
|
-
* Persona Service supplies layer 2; everything below is the manifest's
|
|
381
|
-
* contribution.
|
|
387
|
+
* Experience system message; `callTemplates` supplies layer 4 (call
|
|
388
|
+
* instructions) and optional layer-7 return schemas per template id.
|
|
382
389
|
*/
|
|
383
390
|
export const PromptSlotsSchema = z.object({
|
|
384
391
|
experienceSystem: PromptSlotValueSchema,
|
|
385
|
-
|
|
386
|
-
outputSchemas: z.record(CallKindSchema, JsonValueSchema).optional(),
|
|
392
|
+
callTemplates: z.record(z.string().min(1), CallTemplateSchema),
|
|
387
393
|
});
|
|
388
394
|
|
|
389
|
-
// -----------------------------------------------------------------------------
|
|
390
|
-
// Fallback responses
|
|
391
|
-
// -----------------------------------------------------------------------------
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Pre-written copy the Runtime emits when an inference call fails or
|
|
395
|
-
* the safety pipeline blocks the model output. Keyed per `callKind` so
|
|
396
|
-
* a host_judge fallback is distinct from a host_recap fallback.
|
|
397
|
-
*/
|
|
398
|
-
export const FallbackResponsesSchema = z.record(CallKindSchema, z.string().min(1));
|
|
399
|
-
|
|
400
395
|
// -----------------------------------------------------------------------------
|
|
401
396
|
// Content rating
|
|
402
397
|
// -----------------------------------------------------------------------------
|
|
@@ -554,10 +549,14 @@ export const ManifestSchema = z.object({
|
|
|
554
549
|
scoring: ScoringSchema,
|
|
555
550
|
lifecyclePolicies: z.array(LifecyclePolicySchema),
|
|
556
551
|
promptSlots: PromptSlotsSchema,
|
|
557
|
-
fallbackResponses: FallbackResponsesSchema,
|
|
558
552
|
widgetDependencies: z.array(z.string().min(1)),
|
|
559
553
|
contentRating: ContentRatingSchema,
|
|
560
554
|
portalMetadata: PortalMetadataSchema,
|
|
555
|
+
/**
|
|
556
|
+
* Host-configurable knobs chosen at session provision (Portal launch
|
|
557
|
+
* dialog). Defaults are applied when the host omits a value.
|
|
558
|
+
*/
|
|
559
|
+
sessionSettings: SessionSettingsSchema.optional(),
|
|
561
560
|
/** Reserved for Phase 2; current Runtime ignores these. */
|
|
562
561
|
bundleCompatible: z.boolean().default(false).optional(),
|
|
563
562
|
/** Reserved for Phase 2; current Runtime ignores these. */
|
|
@@ -590,9 +589,14 @@ export type LifecycleSituation = z.infer<typeof LifecycleSituationSchema>;
|
|
|
590
589
|
export type LifecycleAction = z.infer<typeof LifecycleActionSchema>;
|
|
591
590
|
export type LifecyclePolicy = z.infer<typeof LifecyclePolicySchema>;
|
|
592
591
|
export type PromptSlotValue = z.infer<typeof PromptSlotValueSchema>;
|
|
593
|
-
export type
|
|
592
|
+
export type CallTemplate = z.infer<typeof CallTemplateSchema>;
|
|
594
593
|
export type PromptSlots = z.infer<typeof PromptSlotsSchema>;
|
|
595
|
-
|
|
594
|
+
|
|
595
|
+
export const getCallTemplate = (
|
|
596
|
+
manifest: Manifest,
|
|
597
|
+
templateId: string,
|
|
598
|
+
): CallTemplate | undefined =>
|
|
599
|
+
manifest.promptSlots.callTemplates[templateId];
|
|
596
600
|
export type ContentRatingFloor = z.infer<typeof ContentRatingFloorSchema>;
|
|
597
601
|
export type ContentRatingAudience = z.infer<typeof ContentRatingAudienceSchema>;
|
|
598
602
|
export type ExperienceContentRatingTier = z.infer<
|
|
@@ -603,5 +607,6 @@ export type OccasionTag = z.infer<typeof OccasionTagSchema>;
|
|
|
603
607
|
export type GameplayImage = z.infer<typeof GameplayImageSchema>;
|
|
604
608
|
export type GameplayVideo = z.infer<typeof GameplayVideoSchema>;
|
|
605
609
|
export type PortalMetadata = z.infer<typeof PortalMetadataSchema>;
|
|
610
|
+
export type SessionSettings = z.infer<typeof SessionSettingsSchema>;
|
|
606
611
|
export type BundleContextSlots = z.infer<typeof BundleContextSlotsSchema>;
|
|
607
612
|
export type Manifest = z.infer<typeof ManifestSchema>;
|
package/src/phase.test.ts
CHANGED
|
@@ -49,6 +49,10 @@ describe('CollectionRuleSchema', () => {
|
|
|
49
49
|
expect(CollectionRuleSchema.safeParse({ kind: 'all_respond' }).success).toBe(
|
|
50
50
|
true,
|
|
51
51
|
);
|
|
52
|
+
expect(
|
|
53
|
+
CollectionRuleSchema.safeParse({ kind: 'all_respond_or_timeout', ms: 45_000 })
|
|
54
|
+
.success,
|
|
55
|
+
).toBe(true);
|
|
52
56
|
expect(
|
|
53
57
|
CollectionRuleSchema.safeParse({ kind: 'first_respond', count: 3 })
|
|
54
58
|
.success,
|
|
@@ -59,6 +63,13 @@ describe('CollectionRuleSchema', () => {
|
|
|
59
63
|
expect(CollectionRuleSchema.safeParse({ kind: 'manual' }).success).toBe(
|
|
60
64
|
true,
|
|
61
65
|
);
|
|
66
|
+
expect(
|
|
67
|
+
CollectionRuleSchema.safeParse({
|
|
68
|
+
kind: 'after_tts_completes',
|
|
69
|
+
timeout: 60_000,
|
|
70
|
+
buffer: 2_000,
|
|
71
|
+
}).success,
|
|
72
|
+
).toBe(true);
|
|
62
73
|
});
|
|
63
74
|
|
|
64
75
|
it('rejects a zero-ms timeout (would deadlock the Runtime)', () => {
|
|
@@ -143,7 +154,7 @@ describe('PhaseSchema', () => {
|
|
|
143
154
|
...baseline,
|
|
144
155
|
sideEffects: [
|
|
145
156
|
{ kind: 'state_write', patches: [{ op: 'replace', path: '/x', value: 1 }] },
|
|
146
|
-
{ kind: 'inference',
|
|
157
|
+
{ kind: 'inference', templateId: 'host_judge' },
|
|
147
158
|
],
|
|
148
159
|
});
|
|
149
160
|
expect(result.success).toBe(true);
|
|
@@ -159,6 +170,26 @@ describe('PhaseSchema', () => {
|
|
|
159
170
|
});
|
|
160
171
|
expect(result.success).toBe(false);
|
|
161
172
|
});
|
|
173
|
+
|
|
174
|
+
it('accepts optional leadInSpeech variants', () => {
|
|
175
|
+
const result = PhaseSchema.safeParse({
|
|
176
|
+
...baseline,
|
|
177
|
+
leadInSpeech: ['Welcome.', 'Let us begin.'],
|
|
178
|
+
});
|
|
179
|
+
expect(result.success).toBe(true);
|
|
180
|
+
if (result.success) {
|
|
181
|
+
expect(result.data.leadInSpeech).toEqual([
|
|
182
|
+
'Welcome.',
|
|
183
|
+
'Let us begin.',
|
|
184
|
+
]);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('rejects empty leadInSpeech lines', () => {
|
|
189
|
+
expect(
|
|
190
|
+
PhaseSchema.safeParse({ ...baseline, leadInSpeech: [''] }).success,
|
|
191
|
+
).toBe(false);
|
|
192
|
+
});
|
|
162
193
|
});
|
|
163
194
|
|
|
164
195
|
describe('WorkflowSideEffectSchema (chunk B8c)', () => {
|
|
@@ -181,25 +212,24 @@ describe('WorkflowSideEffectSchema (chunk B8c)', () => {
|
|
|
181
212
|
it('accepts an inference effect with a targetPath JSON-Pointer', () => {
|
|
182
213
|
const result = WorkflowSideEffectSchema.safeParse({
|
|
183
214
|
kind: 'inference',
|
|
184
|
-
|
|
185
|
-
qualityTier: 'standard',
|
|
215
|
+
templateId: 'host_judge',
|
|
186
216
|
targetPath: '/session/inference/host_judge',
|
|
187
217
|
});
|
|
188
218
|
expect(result.success).toBe(true);
|
|
189
219
|
});
|
|
190
220
|
|
|
191
|
-
it('
|
|
221
|
+
it('accepts any non-empty templateId string', () => {
|
|
192
222
|
const result = WorkflowSideEffectSchema.safeParse({
|
|
193
223
|
kind: 'inference',
|
|
194
|
-
|
|
224
|
+
templateId: 'totally_made_up',
|
|
195
225
|
});
|
|
196
|
-
expect(result.success).toBe(
|
|
226
|
+
expect(result.success).toBe(true);
|
|
197
227
|
});
|
|
198
228
|
|
|
199
229
|
it('rejects an inference effect with a malformed targetPath', () => {
|
|
200
230
|
const result = WorkflowSideEffectSchema.safeParse({
|
|
201
231
|
kind: 'inference',
|
|
202
|
-
|
|
232
|
+
templateId: 'host_judge',
|
|
203
233
|
targetPath: 'not-a-pointer',
|
|
204
234
|
});
|
|
205
235
|
expect(result.success).toBe(false);
|
package/src/phase.ts
CHANGED
|
@@ -70,6 +70,15 @@ export const InputSetSchema = z.object({
|
|
|
70
70
|
*/
|
|
71
71
|
export const CollectionRuleSchema = z.discriminatedUnion('kind', [
|
|
72
72
|
z.object({ kind: z.literal('all_respond') }),
|
|
73
|
+
z.object({
|
|
74
|
+
kind: z.literal('all_respond_or_timeout'),
|
|
75
|
+
ms: z.number().int().positive(),
|
|
76
|
+
}),
|
|
77
|
+
z.object({
|
|
78
|
+
kind: z.literal('after_tts_completes'),
|
|
79
|
+
timeout: z.number().int().positive(),
|
|
80
|
+
buffer: z.number().int().nonnegative(),
|
|
81
|
+
}),
|
|
73
82
|
z.object({
|
|
74
83
|
kind: z.literal('first_respond'),
|
|
75
84
|
count: z.number().int().positive(),
|
|
@@ -97,35 +106,6 @@ export const TransitionSchema = z.object({
|
|
|
97
106
|
// Phase side-effect declarations (chunk B8c)
|
|
98
107
|
// -----------------------------------------------------------------------------
|
|
99
108
|
|
|
100
|
-
/**
|
|
101
|
-
* Quality-tier enum lifted here (chunk B8c) so the side-effect schema
|
|
102
|
-
* can carry an optional `qualityTier` without requiring a circular
|
|
103
|
-
* import. Mirrors `QualityTierSchema` re-exported from `manifest.ts`.
|
|
104
|
-
*/
|
|
105
|
-
const PhaseSideEffectQualityTierSchema = z.enum([
|
|
106
|
-
'fast',
|
|
107
|
-
'standard',
|
|
108
|
-
'premium',
|
|
109
|
-
'creative',
|
|
110
|
-
]);
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Known `callKind` set lifted here for the same reason as
|
|
114
|
-
* {@link PhaseSideEffectQualityTierSchema}. Kept exactly in lock-step
|
|
115
|
-
* with `CallKindSchema` in `manifest.ts`; `manifest.ts` imports this
|
|
116
|
-
* symbol so they cannot drift.
|
|
117
|
-
*/
|
|
118
|
-
export const CallKindSchema = z.enum([
|
|
119
|
-
'host_open_phase',
|
|
120
|
-
'host_judge',
|
|
121
|
-
'host_resolve',
|
|
122
|
-
'host_recap',
|
|
123
|
-
'judge_funniness',
|
|
124
|
-
'narrate_event',
|
|
125
|
-
'classify',
|
|
126
|
-
'compose_clue',
|
|
127
|
-
]);
|
|
128
|
-
|
|
129
109
|
/**
|
|
130
110
|
* RFC 6901 JSON-Pointer validator. Accepts the empty pointer `""`
|
|
131
111
|
* (root) and any pointer starting with `/`. We do NOT enforce
|
|
@@ -161,17 +141,16 @@ export const StateWriteSideEffectSchema = z.object({
|
|
|
161
141
|
});
|
|
162
142
|
|
|
163
143
|
/**
|
|
164
|
-
* `inference` side effect — dispatch a Gateway call. `
|
|
165
|
-
*
|
|
166
|
-
* variables for template interpolation.
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* `/session/inference/<
|
|
144
|
+
* `inference` side effect — dispatch a Gateway call. `templateId`
|
|
145
|
+
* keys a manifest `callTemplates` entry; `slots` carries per-call
|
|
146
|
+
* variables for template interpolation. Quality tier and return schema
|
|
147
|
+
* come from the template definition. `targetPath` is an RFC 6901
|
|
148
|
+
* JSON-Pointer the Runtime writes the inference output to once the
|
|
149
|
+
* Gateway returns (default: `/session/inference/<templateId>`).
|
|
170
150
|
*/
|
|
171
151
|
export const InferenceSideEffectSchema = z.object({
|
|
172
152
|
kind: z.literal('inference'),
|
|
173
|
-
|
|
174
|
-
qualityTier: PhaseSideEffectQualityTierSchema.optional(),
|
|
153
|
+
templateId: z.string().min(1),
|
|
175
154
|
slots: z.record(z.unknown()).optional(),
|
|
176
155
|
targetPath: JsonPointerSchema.optional(),
|
|
177
156
|
});
|
|
@@ -254,7 +233,16 @@ export type WorkflowSideEffect = z.infer<typeof WorkflowSideEffectSchema>;
|
|
|
254
233
|
* fires the sandbox `onRoundEnd` hook on phase EXIT (before
|
|
255
234
|
* the swap). Used by multi-round Experiences (Rashomon) to
|
|
256
235
|
* trigger end-of-round bookkeeping in the bundle.
|
|
236
|
+
* - `leadInSpeech?: string[]` — optional static host-voice lines
|
|
237
|
+
* spoken on phase enter. Omitted or empty skips platform TTS.
|
|
238
|
+
* When multiple lines are declared the runtime picks one at
|
|
239
|
+
* random (deterministic per transition). The number of variants
|
|
240
|
+
* is unbounded — Experiences may ship large pools of filler lines
|
|
241
|
+
* to keep the host fresh across many rounds. Richer (dynamic)
|
|
242
|
+
* lead-ins still belong in the sandbox `onPhaseEnter` hook.
|
|
257
243
|
*/
|
|
244
|
+
const LEAD_IN_SPEECH_MAX_CHARS = 500;
|
|
245
|
+
|
|
258
246
|
export const PhaseSchema = z.lazy(() =>
|
|
259
247
|
z.object({
|
|
260
248
|
id: z.string().min(1),
|
|
@@ -265,6 +253,9 @@ export const PhaseSchema = z.lazy(() =>
|
|
|
265
253
|
subPhases: z.record(z.string().min(1), PhaseSchema).optional(),
|
|
266
254
|
computeScoreOnEnter: z.boolean().optional(),
|
|
267
255
|
endsRound: z.boolean().optional(),
|
|
256
|
+
leadInSpeech: z
|
|
257
|
+
.array(z.string().min(1).max(LEAD_IN_SPEECH_MAX_CHARS))
|
|
258
|
+
.optional(),
|
|
268
259
|
}),
|
|
269
260
|
) as z.ZodType<ManifestPhase>;
|
|
270
261
|
|
|
@@ -301,4 +292,5 @@ export type ManifestPhase = {
|
|
|
301
292
|
readonly subPhases?: Readonly<Record<string, ManifestPhase>>;
|
|
302
293
|
readonly computeScoreOnEnter?: boolean;
|
|
303
294
|
readonly endsRound?: boolean;
|
|
295
|
+
readonly leadInSpeech?: readonly string[];
|
|
304
296
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
resolveSessionSettings,
|
|
5
|
+
validateSessionSettingsOverrides,
|
|
6
|
+
} from './session-settings.js';
|
|
7
|
+
|
|
8
|
+
describe('session-settings', () => {
|
|
9
|
+
const definitions = [
|
|
10
|
+
{
|
|
11
|
+
key: 'totalRounds',
|
|
12
|
+
type: 'number' as const,
|
|
13
|
+
label: 'Rounds',
|
|
14
|
+
min: 3,
|
|
15
|
+
max: 12,
|
|
16
|
+
default: 8,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
key: 'hardMode',
|
|
20
|
+
type: 'boolean' as const,
|
|
21
|
+
label: 'Hard mode',
|
|
22
|
+
default: false,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
it('resolves defaults when overrides are omitted', () => {
|
|
27
|
+
expect(resolveSessionSettings(definitions, undefined)).toEqual({
|
|
28
|
+
totalRounds: 8,
|
|
29
|
+
hardMode: false,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('validates in-range overrides', () => {
|
|
34
|
+
const result = validateSessionSettingsOverrides(definitions, {
|
|
35
|
+
totalRounds: 10,
|
|
36
|
+
hardMode: true,
|
|
37
|
+
});
|
|
38
|
+
expect(result.ok).toBe(true);
|
|
39
|
+
if (result.ok) {
|
|
40
|
+
expect(result.settings).toEqual({ totalRounds: 10, hardMode: true });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('rejects unknown keys', () => {
|
|
45
|
+
const result = validateSessionSettingsOverrides(definitions, {
|
|
46
|
+
mystery: true,
|
|
47
|
+
});
|
|
48
|
+
expect(result.ok).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-configurable session settings declared in the Experience manifest.
|
|
3
|
+
*
|
|
4
|
+
* Settings are authored in Studio, chosen at provision time (Portal launch
|
|
5
|
+
* dialog), validated against manifest definitions, and written to
|
|
6
|
+
* `/session/settings` before sandbox hooks run.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
|
|
11
|
+
import type { JsonValue } from './manifest.js';
|
|
12
|
+
|
|
13
|
+
const SettingKeySchema = z
|
|
14
|
+
.string()
|
|
15
|
+
.regex(/^[a-z][a-zA-Z0-9_]*$/, 'setting key must be camelCase');
|
|
16
|
+
|
|
17
|
+
const SessionSettingDefinitionBaseSchema = z.object({
|
|
18
|
+
key: SettingKeySchema,
|
|
19
|
+
label: z.string().min(1),
|
|
20
|
+
description: z.string().optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export const SessionSettingNumberDefinitionSchema =
|
|
24
|
+
SessionSettingDefinitionBaseSchema.extend({
|
|
25
|
+
type: z.literal('number'),
|
|
26
|
+
default: z.number(),
|
|
27
|
+
min: z.number().optional(),
|
|
28
|
+
max: z.number().optional(),
|
|
29
|
+
step: z.number().positive().optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const SessionSettingBooleanDefinitionSchema =
|
|
33
|
+
SessionSettingDefinitionBaseSchema.extend({
|
|
34
|
+
type: z.literal('boolean'),
|
|
35
|
+
default: z.boolean(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export const SessionSettingEnumDefinitionSchema =
|
|
39
|
+
SessionSettingDefinitionBaseSchema.extend({
|
|
40
|
+
type: z.literal('enum'),
|
|
41
|
+
options: z
|
|
42
|
+
.array(
|
|
43
|
+
z.object({
|
|
44
|
+
value: z.string().min(1),
|
|
45
|
+
label: z.string().min(1),
|
|
46
|
+
}),
|
|
47
|
+
)
|
|
48
|
+
.min(1),
|
|
49
|
+
default: z.string().min(1),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export const SessionSettingDefinitionSchema = z.discriminatedUnion('type', [
|
|
53
|
+
SessionSettingNumberDefinitionSchema,
|
|
54
|
+
SessionSettingBooleanDefinitionSchema,
|
|
55
|
+
SessionSettingEnumDefinitionSchema,
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
export const SessionSettingsSchema = z.object({
|
|
59
|
+
definitions: z.array(SessionSettingDefinitionSchema).default([]),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export type SessionSettingDefinition = z.infer<
|
|
63
|
+
typeof SessionSettingDefinitionSchema
|
|
64
|
+
>;
|
|
65
|
+
export type SessionSettings = z.infer<typeof SessionSettingsSchema>;
|
|
66
|
+
export type ResolvedSessionSettings = Readonly<Record<string, JsonValue>>;
|
|
67
|
+
|
|
68
|
+
const definitionByKey = (
|
|
69
|
+
definitions: readonly SessionSettingDefinition[],
|
|
70
|
+
): Map<string, SessionSettingDefinition> => {
|
|
71
|
+
const map = new Map<string, SessionSettingDefinition>();
|
|
72
|
+
for (const def of definitions) {
|
|
73
|
+
map.set(def.key, def);
|
|
74
|
+
}
|
|
75
|
+
return map;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const coerceNumber = (value: unknown): number | null => {
|
|
79
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
80
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
81
|
+
const parsed = Number(value);
|
|
82
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const validateNumberValue = (
|
|
88
|
+
def: z.infer<typeof SessionSettingNumberDefinitionSchema>,
|
|
89
|
+
value: number,
|
|
90
|
+
): string | null => {
|
|
91
|
+
if (def.min !== undefined && value < def.min) {
|
|
92
|
+
return `${def.label} must be at least ${def.min}.`;
|
|
93
|
+
}
|
|
94
|
+
if (def.max !== undefined && value > def.max) {
|
|
95
|
+
return `${def.label} must be at most ${def.max}.`;
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Merge manifest defaults with host-provided overrides. Unknown keys are
|
|
102
|
+
* ignored; invalid values fall back to defaults.
|
|
103
|
+
*/
|
|
104
|
+
export const resolveSessionSettings = (
|
|
105
|
+
definitions: readonly SessionSettingDefinition[],
|
|
106
|
+
overrides: Readonly<Record<string, unknown>> | undefined,
|
|
107
|
+
): ResolvedSessionSettings => {
|
|
108
|
+
const resolved: Record<string, JsonValue> = {};
|
|
109
|
+
for (const def of definitions) {
|
|
110
|
+
const raw = overrides?.[def.key];
|
|
111
|
+
if (def.type === 'number') {
|
|
112
|
+
const coerced = raw === undefined ? def.default : coerceNumber(raw);
|
|
113
|
+
const value =
|
|
114
|
+
coerced === null
|
|
115
|
+
? def.default
|
|
116
|
+
: validateNumberValue(def, coerced) === null
|
|
117
|
+
? coerced
|
|
118
|
+
: def.default;
|
|
119
|
+
resolved[def.key] = value;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (def.type === 'boolean') {
|
|
123
|
+
resolved[def.key] =
|
|
124
|
+
typeof raw === 'boolean' ? raw : def.default;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const allowed = new Set(def.options.map((opt) => opt.value));
|
|
128
|
+
resolved[def.key] =
|
|
129
|
+
typeof raw === 'string' && allowed.has(raw) ? raw : def.default;
|
|
130
|
+
}
|
|
131
|
+
return resolved;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type SessionSettingsValidationResult =
|
|
135
|
+
| { readonly ok: true; readonly settings: ResolvedSessionSettings }
|
|
136
|
+
| { readonly ok: false; readonly message: string };
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Strict validation for provision-time overrides. Rejects unknown keys
|
|
140
|
+
* and out-of-range values.
|
|
141
|
+
*/
|
|
142
|
+
export const validateSessionSettingsOverrides = (
|
|
143
|
+
definitions: readonly SessionSettingDefinition[],
|
|
144
|
+
overrides: Readonly<Record<string, unknown>> | undefined,
|
|
145
|
+
): SessionSettingsValidationResult => {
|
|
146
|
+
if (!overrides || Object.keys(overrides).length === 0) {
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
settings: resolveSessionSettings(definitions, undefined),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const defs = definitionByKey(definitions);
|
|
154
|
+
for (const key of Object.keys(overrides)) {
|
|
155
|
+
if (!defs.has(key)) {
|
|
156
|
+
return { ok: false, message: `Unknown setting "${key}".` };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const def of definitions) {
|
|
161
|
+
const raw = overrides[def.key];
|
|
162
|
+
if (raw === undefined) continue;
|
|
163
|
+
|
|
164
|
+
if (def.type === 'number') {
|
|
165
|
+
const coerced = coerceNumber(raw);
|
|
166
|
+
if (coerced === null) {
|
|
167
|
+
return { ok: false, message: `${def.label} must be a number.` };
|
|
168
|
+
}
|
|
169
|
+
const err = validateNumberValue(def, coerced);
|
|
170
|
+
if (err) return { ok: false, message: err };
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (def.type === 'boolean') {
|
|
175
|
+
if (typeof raw !== 'boolean') {
|
|
176
|
+
return { ok: false, message: `${def.label} must be true or false.` };
|
|
177
|
+
}
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (typeof raw !== 'string' || !def.options.some((opt) => opt.value === raw)) {
|
|
182
|
+
return { ok: false, message: `${def.label} has an invalid option.` };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
settings: resolveSessionSettings(definitions, overrides),
|
|
189
|
+
};
|
|
190
|
+
};
|
package/src/validate.test.ts
CHANGED
|
@@ -104,6 +104,31 @@ describe('validateManifest — structural: initial phase', () => {
|
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
describe('validateManifest — structural: abort phase', () => {
|
|
108
|
+
it('accepts a manifest with no abortPhaseId (optional)', () => {
|
|
109
|
+
const fixture = cloneFixture();
|
|
110
|
+
expect(validateManifest(fixture).ok).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('accepts a valid abortPhaseId that references a declared phase', () => {
|
|
114
|
+
const fixture = cloneFixture();
|
|
115
|
+
fixture.workflow.abortPhaseId = 'resolved';
|
|
116
|
+
expect(validateManifest(fixture).ok).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('rejects an abortPhaseId that is not a declared phase', () => {
|
|
120
|
+
const fixture = cloneFixture();
|
|
121
|
+
fixture.workflow.abortPhaseId = 'nonexistent';
|
|
122
|
+
const result = validateManifest(fixture);
|
|
123
|
+
expect(result.ok).toBe(false);
|
|
124
|
+
if (!result.ok) {
|
|
125
|
+
expectErrorCode(result.error, 'unknown_abort_phase');
|
|
126
|
+
const issue = result.error.find((e) => e.code === 'unknown_abort_phase');
|
|
127
|
+
expect(issue?.path).toEqual(['workflow', 'abortPhaseId']);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
107
132
|
describe('validateManifest — structural: duplicate phase ids', () => {
|
|
108
133
|
it('rejects a manifest where two phases share an id', () => {
|
|
109
134
|
const fixture = cloneFixture();
|
|
@@ -272,6 +297,10 @@ describe('validateManifest — structural: phase side effects (chunk B8c)', () =
|
|
|
272
297
|
|
|
273
298
|
it('accepts a phase with valid scoring + inference side effects', () => {
|
|
274
299
|
const fixture = cloneFixture();
|
|
300
|
+
fixture.promptSlots.callTemplates.host_judge = {
|
|
301
|
+
prompt: 'Judge answers.',
|
|
302
|
+
qualityTier: 'standard',
|
|
303
|
+
};
|
|
275
304
|
patchPhase(fixture, 'guess', {
|
|
276
305
|
sideEffects: [
|
|
277
306
|
{
|
|
@@ -282,7 +311,7 @@ describe('validateManifest — structural: phase side effects (chunk B8c)', () =
|
|
|
282
311
|
},
|
|
283
312
|
{
|
|
284
313
|
kind: 'inference',
|
|
285
|
-
|
|
314
|
+
templateId: 'host_judge',
|
|
286
315
|
targetPath: '/session/inference/host_judge',
|
|
287
316
|
},
|
|
288
317
|
],
|
package/src/validate.ts
CHANGED
|
@@ -63,6 +63,7 @@ import type { ManifestPhase } from './phase.js';
|
|
|
63
63
|
export type ValidationErrorCode =
|
|
64
64
|
| 'schema'
|
|
65
65
|
| 'unknown_initial_phase'
|
|
66
|
+
| 'unknown_abort_phase'
|
|
66
67
|
| 'duplicate_phase_id'
|
|
67
68
|
| 'unknown_transition_target'
|
|
68
69
|
| 'unreachable_phase'
|
|
@@ -70,7 +71,9 @@ export type ValidationErrorCode =
|
|
|
70
71
|
| 'unknown_opportunity_phase'
|
|
71
72
|
| 'unknown_award_dimension'
|
|
72
73
|
| 'unknown_side_effect_scoring_dimension'
|
|
73
|
-
| 'unknown_side_effect_persona_binding'
|
|
74
|
+
| 'unknown_side_effect_persona_binding'
|
|
75
|
+
| 'unknown_side_effect_inference_template'
|
|
76
|
+
| 'call_template_tier_not_allowed';
|
|
74
77
|
|
|
75
78
|
export type ValidationError = {
|
|
76
79
|
readonly code: ValidationErrorCode;
|
|
@@ -135,6 +138,21 @@ const checkInitialPhase = (
|
|
|
135
138
|
}
|
|
136
139
|
};
|
|
137
140
|
|
|
141
|
+
const checkAbortPhase = (
|
|
142
|
+
workflow: Workflow,
|
|
143
|
+
validIds: ReadonlySet<string>,
|
|
144
|
+
out: ValidationError[],
|
|
145
|
+
): void => {
|
|
146
|
+
if (workflow.abortPhaseId === undefined) return;
|
|
147
|
+
if (!validIds.has(workflow.abortPhaseId)) {
|
|
148
|
+
out.push({
|
|
149
|
+
code: 'unknown_abort_phase',
|
|
150
|
+
path: ['workflow', 'abortPhaseId'],
|
|
151
|
+
message: `abortPhaseId '${workflow.abortPhaseId}' is not declared in workflow.phases`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
138
156
|
const checkTransitionTargets = (
|
|
139
157
|
workflow: Workflow,
|
|
140
158
|
validIds: ReadonlySet<string>,
|
|
@@ -276,15 +294,16 @@ const checkAwardDimensions = (
|
|
|
276
294
|
* dispatch time; catching it at manifest validation surfaces
|
|
277
295
|
* the error at publish time instead.
|
|
278
296
|
*
|
|
279
|
-
* `state_write`
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
297
|
+
* `state_write` side effects have no cross-field dependencies the
|
|
298
|
+
* manifest layer can check — the JSON-Patch ops are validated against
|
|
299
|
+
* per-Experience state schemas at apply time. `inference.templateId`
|
|
300
|
+
* is cross-checked against `promptSlots.callTemplates`.
|
|
283
301
|
*/
|
|
284
302
|
const checkPhaseSideEffects = (
|
|
285
303
|
phases: readonly ManifestPhase[],
|
|
286
304
|
dimensionIds: ReadonlySet<string>,
|
|
287
305
|
personaIds: ReadonlySet<string>,
|
|
306
|
+
templateIds: ReadonlySet<string>,
|
|
288
307
|
out: ValidationError[],
|
|
289
308
|
): void => {
|
|
290
309
|
for (let i = 0; i < phases.length; i += 1) {
|
|
@@ -323,11 +342,44 @@ const checkPhaseSideEffects = (
|
|
|
323
342
|
message: `phase '${phase.id}' persona_memory side effect references unbound persona '${effect.personaId}'`,
|
|
324
343
|
});
|
|
325
344
|
}
|
|
345
|
+
} else if (effect.kind === 'inference') {
|
|
346
|
+
if (!templateIds.has(effect.templateId)) {
|
|
347
|
+
out.push({
|
|
348
|
+
code: 'unknown_side_effect_inference_template',
|
|
349
|
+
path: [
|
|
350
|
+
'workflow',
|
|
351
|
+
'phases',
|
|
352
|
+
i,
|
|
353
|
+
'sideEffects',
|
|
354
|
+
j,
|
|
355
|
+
'templateId',
|
|
356
|
+
],
|
|
357
|
+
message: `phase '${phase.id}' inference side effect references unknown call template '${effect.templateId}'`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
326
360
|
}
|
|
327
361
|
}
|
|
328
362
|
}
|
|
329
363
|
};
|
|
330
364
|
|
|
365
|
+
const checkCallTemplateQualityTiers = (
|
|
366
|
+
manifest: Manifest,
|
|
367
|
+
out: ValidationError[],
|
|
368
|
+
): void => {
|
|
369
|
+
const allowed = new Set(manifest.inferenceEnvelope.qualityTiers);
|
|
370
|
+
for (const [templateId, template] of Object.entries(
|
|
371
|
+
manifest.promptSlots.callTemplates,
|
|
372
|
+
)) {
|
|
373
|
+
if (!allowed.has(template.qualityTier)) {
|
|
374
|
+
out.push({
|
|
375
|
+
code: 'call_template_tier_not_allowed',
|
|
376
|
+
path: ['promptSlots', 'callTemplates', templateId, 'qualityTier'],
|
|
377
|
+
message: `call template '${templateId}' uses quality tier '${template.qualityTier}' which is not in inferenceEnvelope.qualityTiers`,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
331
383
|
// -----------------------------------------------------------------------------
|
|
332
384
|
// Public entrypoint
|
|
333
385
|
// -----------------------------------------------------------------------------
|
|
@@ -358,6 +410,7 @@ export const validateManifest = (
|
|
|
358
410
|
|
|
359
411
|
const validIds = checkPhaseIdsUnique(manifest.workflow, errors);
|
|
360
412
|
checkInitialPhase(manifest.workflow, validIds, errors);
|
|
413
|
+
checkAbortPhase(manifest.workflow, validIds, errors);
|
|
361
414
|
checkTransitionTargets(manifest.workflow, validIds, errors);
|
|
362
415
|
checkReachability(manifest.workflow, validIds, errors);
|
|
363
416
|
checkPersonaRolesUnique(manifest.personaBindings, errors);
|
|
@@ -377,8 +430,10 @@ export const validateManifest = (
|
|
|
377
430
|
manifest.workflow.phases,
|
|
378
431
|
dimensionIds,
|
|
379
432
|
personaIds,
|
|
433
|
+
new Set(Object.keys(manifest.promptSlots.callTemplates)),
|
|
380
434
|
errors,
|
|
381
435
|
);
|
|
436
|
+
checkCallTemplateQualityTiers(manifest, errors);
|
|
382
437
|
|
|
383
438
|
if (errors.length > 0) return err(errors);
|
|
384
439
|
return ok(manifest);
|