@iniesta8888/agent-live-dsh-adapter 0.3.0
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/README.md +23 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +539 -0
- package/lib/index.js +1342 -0
- package/lib/types/dsh/src/adapter.d.ts +53 -0
- package/lib/types/dsh/src/client.d.ts +4 -0
- package/lib/types/dsh/src/creator.d.ts +14 -0
- package/lib/types/dsh/src/frame-runtime.d.ts +1046 -0
- package/lib/types/dsh/src/index.d.ts +4 -0
- package/lib/types/src/content/compiler.d.ts +30 -0
- package/lib/types/src/content/graph-validator.d.ts +55 -0
- package/lib/types/src/content/library.d.ts +3 -0
- package/lib/types/src/content/registry.d.ts +33 -0
- package/lib/types/src/content/runtime-content.d.ts +62 -0
- package/lib/types/src/content/schema.d.ts +141 -0
- package/lib/types/src/content/validator.d.ts +34 -0
- package/lib/types/src/core/limits.d.ts +14 -0
- package/lib/types/src/core/mapping.d.ts +16 -0
- package/lib/types/src/core/protocol.d.ts +104 -0
- package/lib/types/src/creator/commands.d.ts +18 -0
- package/lib/types/src/creator/mode.d.ts +9 -0
- package/lib/types/src/creator/service.d.ts +198 -0
- package/lib/types/src/runtime/content-service.d.ts +74 -0
- package/package.json +89 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1342 @@
|
|
|
1
|
+
// src/creator.ts
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
// ../src/core/limits.ts
|
|
6
|
+
var SCENE_LIMITS = Object.freeze({
|
|
7
|
+
agents: 16,
|
|
8
|
+
seats: 8,
|
|
9
|
+
npcs: 12,
|
|
10
|
+
props: 80,
|
|
11
|
+
animatedProps: 24,
|
|
12
|
+
activities: 16,
|
|
13
|
+
effects: 40,
|
|
14
|
+
visibleBubbles: 4,
|
|
15
|
+
queuedBubbles: 8
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// ../src/content/schema.ts
|
|
19
|
+
var OFFICE_SPEC_SCHEMA_VERSION = 1;
|
|
20
|
+
var GENDER_VALUES = ["female", "male", "nonbinary", "unspecified"];
|
|
21
|
+
var POSE_VALUES = ["stand", "sit"];
|
|
22
|
+
var ORIENTATION_VALUES = ["horizontal", "vertical"];
|
|
23
|
+
var WEATHER_VALUES = ["clear", "cloudy", "rain", "snow"];
|
|
24
|
+
var OFFICE_SPEC_DEFAULTS = Object.freeze({
|
|
25
|
+
style: "builtin/pixel-classic",
|
|
26
|
+
agentSkin: "builtin/tiny-developers",
|
|
27
|
+
atmosphere: "builtin/default-atmosphere",
|
|
28
|
+
environment: "builtin/local-office-environment",
|
|
29
|
+
agentProfile: Object.freeze({ template: "builtin/host-agent" }),
|
|
30
|
+
placements: Object.freeze([]),
|
|
31
|
+
npcs: Object.freeze([]),
|
|
32
|
+
activities: Object.freeze([])
|
|
33
|
+
});
|
|
34
|
+
var SPEC_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "kind", "id", "name", "origin", "basePreset", "layout", "style", "agentSkin", "placements", "npcs", "activities", "atmosphere", "environment", "environmentOverrides", "agentProfile", "texts"]);
|
|
35
|
+
var PATCH_KEYS = /* @__PURE__ */ new Set(["schemaVersion", "kind", "id", "base", "name", "components", "placements", "npcs", "activities", "environmentOverrides", "agentProfile", "texts"]);
|
|
36
|
+
var COMPONENT_KEYS = /* @__PURE__ */ new Set(["layout", "style", "agentSkin", "atmosphere", "environment"]);
|
|
37
|
+
var PLACEMENT_KEYS = /* @__PURE__ */ new Set(["id", "component", "slot", "orientation"]);
|
|
38
|
+
var NPC_KEYS = /* @__PURE__ */ new Set(["id", "template", "profile", "name", "title", "gender", "appearance", "spawn", "shift", "pose"]);
|
|
39
|
+
var APPEARANCE_KEYS = /* @__PURE__ */ new Set(["skin", "hair", "shirt", "trim", "badge"]);
|
|
40
|
+
var AGENT_PROFILE_KEYS = /* @__PURE__ */ new Set(["template", "name", "title", "appearance"]);
|
|
41
|
+
var SHIFT_KEYS = /* @__PURE__ */ new Set(["start", "end", "endLatest"]);
|
|
42
|
+
var ENVIRONMENT_KEYS = /* @__PURE__ */ new Set(["clock", "weather", "lighting", "npcSchedule"]);
|
|
43
|
+
var CLOCK_KEYS = /* @__PURE__ */ new Set(["mode", "fixedTime"]);
|
|
44
|
+
var WEATHER_KEYS = /* @__PURE__ */ new Set(["fallback"]);
|
|
45
|
+
var LIGHTING_KEYS = /* @__PURE__ */ new Set(["auto"]);
|
|
46
|
+
var NPC_SCHEDULE_KEYS = /* @__PURE__ */ new Set(["defaultShift", "roleOverrides"]);
|
|
47
|
+
var COLLECTION_PATCH_KEYS = /* @__PURE__ */ new Set(["upsert", "remove"]);
|
|
48
|
+
var ACTIVITY_PATCH_KEYS = /* @__PURE__ */ new Set(["enable", "disable"]);
|
|
49
|
+
function object(value) {
|
|
50
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
51
|
+
}
|
|
52
|
+
function issue(issues, path5, message) {
|
|
53
|
+
issues.push({ path: path5, message });
|
|
54
|
+
}
|
|
55
|
+
function exactKeys(value, allowed, path5, issues) {
|
|
56
|
+
for (const key of Object.keys(value)) if (!allowed.has(key)) issue(issues, `${path5}.${key}`, "unknown field");
|
|
57
|
+
}
|
|
58
|
+
function requiredString(value, path5, issues) {
|
|
59
|
+
if (typeof value !== "string" || value.trim() === "") issue(issues, path5, "must be a non-empty string");
|
|
60
|
+
}
|
|
61
|
+
function optionalString(value, path5, issues) {
|
|
62
|
+
if (value !== void 0) requiredString(value, path5, issues);
|
|
63
|
+
}
|
|
64
|
+
function stringArray(value, path5, issues) {
|
|
65
|
+
if (!Array.isArray(value)) return issue(issues, path5, "must be an array");
|
|
66
|
+
for (let index = 0; index < value.length; index += 1) requiredString(value[index], `${path5}[${index}]`, issues);
|
|
67
|
+
}
|
|
68
|
+
function enumValue(value, allowed, path5, issues, optional = false) {
|
|
69
|
+
if (optional && value === void 0) return;
|
|
70
|
+
if (typeof value !== "string" || !allowed.includes(value)) issue(issues, path5, `must be one of: ${allowed.join(", ")}`);
|
|
71
|
+
}
|
|
72
|
+
function validateShift(value, path5, issues) {
|
|
73
|
+
if (!object(value)) return issue(issues, path5, "must be an object");
|
|
74
|
+
exactKeys(value, SHIFT_KEYS, path5, issues);
|
|
75
|
+
requiredString(value.start, `${path5}.start`, issues);
|
|
76
|
+
requiredString(value.end, `${path5}.end`, issues);
|
|
77
|
+
optionalString(value.endLatest, `${path5}.endLatest`, issues);
|
|
78
|
+
}
|
|
79
|
+
function validateAppearance(value, path5, issues) {
|
|
80
|
+
if (!object(value)) return issue(issues, path5, "must be an object");
|
|
81
|
+
exactKeys(value, APPEARANCE_KEYS, path5, issues);
|
|
82
|
+
for (const [key, color] of Object.entries(value)) optionalString(color, `${path5}.${key}`, issues);
|
|
83
|
+
}
|
|
84
|
+
function validatePlacement(value, path5, issues) {
|
|
85
|
+
if (!object(value)) return issue(issues, path5, "must be an object");
|
|
86
|
+
exactKeys(value, PLACEMENT_KEYS, path5, issues);
|
|
87
|
+
requiredString(value.id, `${path5}.id`, issues);
|
|
88
|
+
requiredString(value.component, `${path5}.component`, issues);
|
|
89
|
+
requiredString(value.slot, `${path5}.slot`, issues);
|
|
90
|
+
enumValue(value.orientation, ORIENTATION_VALUES, `${path5}.orientation`, issues, true);
|
|
91
|
+
}
|
|
92
|
+
function validateNpc(value, path5, issues) {
|
|
93
|
+
if (!object(value)) return issue(issues, path5, "must be an object");
|
|
94
|
+
exactKeys(value, NPC_KEYS, path5, issues);
|
|
95
|
+
requiredString(value.id, `${path5}.id`, issues);
|
|
96
|
+
for (const key of ["template", "profile", "name", "title", "spawn"]) optionalString(value[key], `${path5}.${key}`, issues);
|
|
97
|
+
enumValue(value.gender, GENDER_VALUES, `${path5}.gender`, issues, true);
|
|
98
|
+
enumValue(value.pose, POSE_VALUES, `${path5}.pose`, issues, true);
|
|
99
|
+
if (value.appearance !== void 0) validateAppearance(value.appearance, `${path5}.appearance`, issues);
|
|
100
|
+
if (value.shift !== void 0) validateShift(value.shift, `${path5}.shift`, issues);
|
|
101
|
+
}
|
|
102
|
+
function validateAgentProfile(value, path5, issues) {
|
|
103
|
+
if (!object(value)) return issue(issues, path5, "must be an object");
|
|
104
|
+
exactKeys(value, AGENT_PROFILE_KEYS, path5, issues);
|
|
105
|
+
requiredString(value.template, `${path5}.template`, issues);
|
|
106
|
+
optionalString(value.name, `${path5}.name`, issues);
|
|
107
|
+
optionalString(value.title, `${path5}.title`, issues);
|
|
108
|
+
if (value.appearance !== void 0) validateAppearance(value.appearance, `${path5}.appearance`, issues);
|
|
109
|
+
}
|
|
110
|
+
function validateEnvironment(value, path5, issues) {
|
|
111
|
+
if (!object(value)) return issue(issues, path5, "must be an object");
|
|
112
|
+
exactKeys(value, ENVIRONMENT_KEYS, path5, issues);
|
|
113
|
+
if (value.clock !== void 0) {
|
|
114
|
+
if (!object(value.clock)) issue(issues, `${path5}.clock`, "must be an object");
|
|
115
|
+
else {
|
|
116
|
+
exactKeys(value.clock, CLOCK_KEYS, `${path5}.clock`, issues);
|
|
117
|
+
enumValue(value.clock.mode, ["local", "fixed"], `${path5}.clock.mode`, issues);
|
|
118
|
+
optionalString(value.clock.fixedTime, `${path5}.clock.fixedTime`, issues);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (value.weather !== void 0) {
|
|
122
|
+
if (!object(value.weather)) issue(issues, `${path5}.weather`, "must be an object");
|
|
123
|
+
else {
|
|
124
|
+
exactKeys(value.weather, WEATHER_KEYS, `${path5}.weather`, issues);
|
|
125
|
+
enumValue(value.weather.fallback, WEATHER_VALUES, `${path5}.weather.fallback`, issues);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (value.lighting !== void 0) {
|
|
129
|
+
if (!object(value.lighting)) issue(issues, `${path5}.lighting`, "must be an object");
|
|
130
|
+
else {
|
|
131
|
+
exactKeys(value.lighting, LIGHTING_KEYS, `${path5}.lighting`, issues);
|
|
132
|
+
if (typeof value.lighting.auto !== "boolean") issue(issues, `${path5}.lighting.auto`, "must be a boolean");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (value.npcSchedule !== void 0) {
|
|
136
|
+
if (!object(value.npcSchedule)) issue(issues, `${path5}.npcSchedule`, "must be an object");
|
|
137
|
+
else {
|
|
138
|
+
exactKeys(value.npcSchedule, NPC_SCHEDULE_KEYS, `${path5}.npcSchedule`, issues);
|
|
139
|
+
if (value.npcSchedule.defaultShift !== void 0) validateShift(value.npcSchedule.defaultShift, `${path5}.npcSchedule.defaultShift`, issues);
|
|
140
|
+
if (value.npcSchedule.roleOverrides !== void 0) {
|
|
141
|
+
if (!object(value.npcSchedule.roleOverrides)) issue(issues, `${path5}.npcSchedule.roleOverrides`, "must be an object");
|
|
142
|
+
else for (const [role, shift] of Object.entries(value.npcSchedule.roleOverrides)) validateShift(shift, `${path5}.npcSchedule.roleOverrides.${role}`, issues);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function validateTexts(value, issues) {
|
|
148
|
+
if (!object(value)) return issue(issues, "$.texts", "must be an object");
|
|
149
|
+
if (Object.keys(value).length > 8) issue(issues, "$.texts", "maximum 8 text areas");
|
|
150
|
+
for (const [key, text] of Object.entries(value)) {
|
|
151
|
+
if (!/^[a-z][a-z0-9-]*$/.test(key)) issue(issues, `$.texts.${key}`, "invalid text area id");
|
|
152
|
+
if (typeof text !== "string" || [...text].length > 120 || /[\r\n\u0000-\u001f]/.test(text)) issue(issues, `$.texts.${key}`, "must be single-line plain text of at most 120 characters");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function validateOfficeSpecShape(input) {
|
|
156
|
+
const issues = [];
|
|
157
|
+
if (!object(input)) return [{ path: "$", message: "must be an object" }];
|
|
158
|
+
exactKeys(input, SPEC_KEYS, "$", issues);
|
|
159
|
+
if (input.schemaVersion !== OFFICE_SPEC_SCHEMA_VERSION) issue(issues, "$.schemaVersion", `must equal ${OFFICE_SPEC_SCHEMA_VERSION}`);
|
|
160
|
+
if (input.kind !== "office-spec") issue(issues, "$.kind", "must equal office-spec");
|
|
161
|
+
for (const key of ["id", "name", "layout", "style", "agentSkin", "atmosphere", "environment"]) requiredString(input[key], `$.${key}`, issues);
|
|
162
|
+
enumValue(input.origin, ["official", "custom"], "$.origin", issues);
|
|
163
|
+
optionalString(input.basePreset, "$.basePreset", issues);
|
|
164
|
+
if (!Array.isArray(input.placements)) issue(issues, "$.placements", "must be an array");
|
|
165
|
+
else input.placements.forEach((value, index) => validatePlacement(value, `$.placements[${index}]`, issues));
|
|
166
|
+
if (!Array.isArray(input.npcs)) issue(issues, "$.npcs", "must be an array");
|
|
167
|
+
else input.npcs.forEach((value, index) => validateNpc(value, `$.npcs[${index}]`, issues));
|
|
168
|
+
stringArray(input.activities, "$.activities", issues);
|
|
169
|
+
if (input.environmentOverrides !== void 0) validateEnvironment(input.environmentOverrides, "$.environmentOverrides", issues);
|
|
170
|
+
if (input.agentProfile !== void 0) validateAgentProfile(input.agentProfile, "$.agentProfile", issues);
|
|
171
|
+
if (input.texts !== void 0) validateTexts(input.texts, issues);
|
|
172
|
+
return issues;
|
|
173
|
+
}
|
|
174
|
+
function validateCollectionPatch(value, path5, issues, itemValidator) {
|
|
175
|
+
if (!object(value)) return issue(issues, path5, "must be an object");
|
|
176
|
+
exactKeys(value, COLLECTION_PATCH_KEYS, path5, issues);
|
|
177
|
+
if (value.upsert !== void 0) {
|
|
178
|
+
if (!Array.isArray(value.upsert)) issue(issues, `${path5}.upsert`, "must be an array");
|
|
179
|
+
else value.upsert.forEach((item, index) => itemValidator(item, `${path5}.upsert[${index}]`, issues));
|
|
180
|
+
}
|
|
181
|
+
if (value.remove !== void 0) stringArray(value.remove, `${path5}.remove`, issues);
|
|
182
|
+
}
|
|
183
|
+
function validateOfficePatchShape(input) {
|
|
184
|
+
const issues = [];
|
|
185
|
+
if (!object(input)) return [{ path: "$", message: "must be an object" }];
|
|
186
|
+
exactKeys(input, PATCH_KEYS, "$", issues);
|
|
187
|
+
if (input.schemaVersion !== OFFICE_SPEC_SCHEMA_VERSION) issue(issues, "$.schemaVersion", `must equal ${OFFICE_SPEC_SCHEMA_VERSION}`);
|
|
188
|
+
if (input.kind !== "office-patch") issue(issues, "$.kind", "must equal office-patch");
|
|
189
|
+
requiredString(input.base, "$.base", issues);
|
|
190
|
+
optionalString(input.id, "$.id", issues);
|
|
191
|
+
optionalString(input.name, "$.name", issues);
|
|
192
|
+
if (input.components !== void 0) {
|
|
193
|
+
if (!object(input.components)) issue(issues, "$.components", "must be an object");
|
|
194
|
+
else {
|
|
195
|
+
exactKeys(input.components, COMPONENT_KEYS, "$.components", issues);
|
|
196
|
+
if (input.components.layout !== void 0) {
|
|
197
|
+
issue(issues, "$.components.layout", "an Office keeps its room; edit the Office that already uses that layout instead");
|
|
198
|
+
}
|
|
199
|
+
for (const [key, value] of Object.entries(input.components)) optionalString(value, `$.components.${key}`, issues);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (input.placements !== void 0) validateCollectionPatch(input.placements, "$.placements", issues, validatePlacement);
|
|
203
|
+
if (input.npcs !== void 0) validateCollectionPatch(input.npcs, "$.npcs", issues, validateNpc);
|
|
204
|
+
if (input.activities !== void 0) {
|
|
205
|
+
if (!object(input.activities)) issue(issues, "$.activities", "must be an object");
|
|
206
|
+
else {
|
|
207
|
+
exactKeys(input.activities, ACTIVITY_PATCH_KEYS, "$.activities", issues);
|
|
208
|
+
if (input.activities.enable !== void 0) stringArray(input.activities.enable, "$.activities.enable", issues);
|
|
209
|
+
if (input.activities.disable !== void 0) stringArray(input.activities.disable, "$.activities.disable", issues);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (input.environmentOverrides !== void 0 && input.environmentOverrides !== null) validateEnvironment(input.environmentOverrides, "$.environmentOverrides", issues);
|
|
213
|
+
if (input.agentProfile !== void 0 && input.agentProfile !== null) validateAgentProfile(input.agentProfile, "$.agentProfile", issues);
|
|
214
|
+
if (input.texts !== void 0 && input.texts !== null) validateTexts(input.texts, issues);
|
|
215
|
+
return issues;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ../src/content/graph-validator.ts
|
|
219
|
+
var WORK_CAPABILITIES = ["research", "create", "compute", "plan", "communicate", "collaborate"];
|
|
220
|
+
var DYNAMIC_ACTIVITY_TARGETS = /* @__PURE__ */ new Set(["near-colleague"]);
|
|
221
|
+
function namedInstance(requirement) {
|
|
222
|
+
if (typeof requirement === "string") return requirement;
|
|
223
|
+
if (requirement && typeof requirement.prop === "string") return requirement.prop;
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
function resolveActivityRequirements(requires, instances, capabilitiesOf, activity) {
|
|
227
|
+
const table = [...instances];
|
|
228
|
+
const bindings = [];
|
|
229
|
+
const issues = [];
|
|
230
|
+
for (const requirement of requires ?? []) {
|
|
231
|
+
const named = namedInstance(requirement);
|
|
232
|
+
if (named !== void 0) {
|
|
233
|
+
const present = table.some(([instanceId]) => instanceId === named);
|
|
234
|
+
bindings.push(present ? named : null);
|
|
235
|
+
if (!present) issues.push({ code: "missing-activity-prop", path: "$", message: `${activity} \u7F3A\u5C11 Prop\uFF1A${named}` });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
const capability = requirement && typeof requirement.capability === "string" ? requirement.capability : void 0;
|
|
239
|
+
if (!capability) {
|
|
240
|
+
bindings.push(null);
|
|
241
|
+
issues.push({ code: "invalid-activity-requirement", path: "$", message: `${activity} \u7684\u4F9D\u8D56\u5FC5\u987B\u662F\u5177\u540D\u9053\u5177\u6216\u80FD\u529B\u9700\u6C42` });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const match = table.find(([, type]) => capabilitiesOf(type).includes(capability));
|
|
245
|
+
bindings.push(match ? match[0] : null);
|
|
246
|
+
if (!match) issues.push({ code: "missing-activity-capability", path: "$", message: `${activity} \u9700\u8981 ${capability} \u80FD\u529B\uFF0C\u5F53\u524D\u529E\u516C\u5BA4\u6CA1\u6709\u63D0\u4F9B\u8BE5\u80FD\u529B\u7684\u9053\u5177` });
|
|
247
|
+
}
|
|
248
|
+
return { bindings, issues };
|
|
249
|
+
}
|
|
250
|
+
function assertManifest(value, kind) {
|
|
251
|
+
const manifest = value;
|
|
252
|
+
if (!manifest || manifest.schemaVersion !== 1 || manifest.kind !== kind || !manifest.id || !manifest.version) {
|
|
253
|
+
throw new Error(`\u65E0\u6548\u7684 ${kind} \u5185\u5BB9\u6E05\u5355`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function manifestKindFor(key) {
|
|
257
|
+
if (key === "agentSkin") return "agent-skin";
|
|
258
|
+
if (key === "lifeActivities") return "life-activities";
|
|
259
|
+
return key;
|
|
260
|
+
}
|
|
261
|
+
function validClock(value) {
|
|
262
|
+
if (typeof value !== "string" || !/^\d{2}:\d{2}$/.test(value)) return false;
|
|
263
|
+
const [hour, minute] = value.split(":").map(Number);
|
|
264
|
+
return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59;
|
|
265
|
+
}
|
|
266
|
+
function shiftIssue(value, code, path5, label) {
|
|
267
|
+
if (!value || !validClock(value.start) || !validClock(value.end)) return { code, path: path5, message: `${label} \u5FC5\u987B\u63D0\u4F9B\u6709\u6548\u7684 HH:MM \u8D77\u6B62\u65F6\u95F4` };
|
|
268
|
+
if (value.endLatest !== void 0 && !validClock(value.endLatest)) return { code, path: `${path5}.endLatest`, message: `${label} \u7684\u6700\u665A\u4E0B\u73ED\u65F6\u95F4\u5FC5\u987B\u662F\u6709\u6548\u7684 HH:MM` };
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
function layoutIssues(layout, propTypeNames) {
|
|
272
|
+
if (!layout || typeof layout !== "object") return [{ code: "missing-layout", path: "$.layout", message: "Layout \u7F3A\u5931" }];
|
|
273
|
+
const issues = [];
|
|
274
|
+
if (layout.contract !== "single-office-v1") issues.push({ code: "invalid-layout-contract", path: "$.layout", message: `\u4E0D\u652F\u6301\u7684 Layout \u5408\u540C\uFF1A${layout.contract}` });
|
|
275
|
+
if (layout.canvas?.width !== 384 || layout.canvas?.height !== 216) issues.push({ code: "invalid-layout-canvas", path: "$.layout", message: "single-office-v1 \u5FC5\u987B\u4F7F\u7528 384\xD7216 \u903B\u8F91\u753B\u5E03" });
|
|
276
|
+
if (!Array.isArray(layout.seats) || layout.seats.length !== 8) issues.push({ code: "invalid-layout-seats", path: "$.layout", message: "Demo Layout \u5FC5\u987B\u63D0\u4F9B 8 \u4E2A\u5EA7\u4F4D" });
|
|
277
|
+
if (!Array.isArray(layout.navigation?.lanes) || !layout.navigation.lanes.length) issues.push({ code: "invalid-layout-navigation", path: "$.layout", message: "Layout \u7F3A\u5C11\u5BFC\u822A\u901A\u9053" });
|
|
278
|
+
const propTypes = new Set(propTypeNames);
|
|
279
|
+
const textIds = /* @__PURE__ */ new Set();
|
|
280
|
+
if (layout.textSlots !== void 0 && (!Array.isArray(layout.textSlots) || layout.textSlots.length > 8)) {
|
|
281
|
+
issues.push({ code: "invalid-text-slots", path: "$.layout.textSlots", message: "maximum 8 text areas" });
|
|
282
|
+
} else for (const slot of layout.textSlots ?? []) {
|
|
283
|
+
const valid = slot && typeof slot.id === "string" && /^[a-z][a-z0-9-]*$/.test(slot.id) && !textIds.has(slot.id) && [slot.x, slot.y, slot.width, slot.height, slot.maxLength].every(Number.isFinite) && slot.x >= 0 && slot.y >= 0 && slot.width >= 12 && slot.height >= 9 && slot.x + slot.width <= 384 && slot.y + slot.height <= 216 && slot.maxLength >= 1 && slot.maxLength <= 120 && [slot.text, slot.defaultText].every((text) => text === void 0 || typeof text === "string" && [...text].length <= slot.maxLength);
|
|
284
|
+
if (!valid) issues.push({ code: "invalid-text-slot", path: "$.layout.textSlots", message: "invalid, duplicate or out-of-bounds text area" });
|
|
285
|
+
if (slot?.id) textIds.add(slot.id);
|
|
286
|
+
}
|
|
287
|
+
const propInstances = /* @__PURE__ */ new Set();
|
|
288
|
+
for (const instance of layout.propInstances ?? []) {
|
|
289
|
+
if (!propTypes.has(instance?.type)) issues.push({ code: "unknown-layout-prop", path: "$.layout.propInstances", message: `\u672A\u77E5 Prop Type\uFF1A${instance?.type}` });
|
|
290
|
+
if (!instance?.id || propInstances.has(instance.id)) issues.push({ code: "invalid-layout-prop", path: "$.layout.propInstances", message: `\u91CD\u590D\u6216\u65E0\u6548\u7684 Prop \u5B9E\u4F8B\uFF1A${instance?.id ?? "\u2014"}` });
|
|
291
|
+
propInstances.add(instance?.id);
|
|
292
|
+
}
|
|
293
|
+
for (const capability of WORK_CAPABILITIES) {
|
|
294
|
+
if (!layout.stations?.[capability]) issues.push({ code: "missing-layout-station", path: `$.layout.stations.${capability}`, message: `Layout \u7F3A\u5C11\u5DE5\u4F5C\u80FD\u529B\uFF1A${capability}` });
|
|
295
|
+
}
|
|
296
|
+
return issues;
|
|
297
|
+
}
|
|
298
|
+
function graphIssues(content) {
|
|
299
|
+
const issues = [];
|
|
300
|
+
for (const [key, value] of Object.entries(content ?? {})) {
|
|
301
|
+
if (key === "preset" || key === "agentProfile") continue;
|
|
302
|
+
try {
|
|
303
|
+
assertManifest(value, manifestKindFor(key));
|
|
304
|
+
} catch (error) {
|
|
305
|
+
issues.push({ code: "invalid-manifest", path: `$.${key}`, message: error.message });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const profile = content?.agentProfile;
|
|
309
|
+
if (!profile || typeof profile !== "object" || typeof profile.template !== "string" || !profile.template || !profile.appearance || typeof profile.appearance !== "object" || Array.isArray(profile.appearance)) {
|
|
310
|
+
issues.push({ code: "invalid-agent-profile", path: "$.agentProfile", message: "\u65E0\u6548\u7684 Agent Profile" });
|
|
311
|
+
}
|
|
312
|
+
const layout = content?.layout;
|
|
313
|
+
issues.push(...layoutIssues(layout, Object.keys(content?.props?.types ?? {})));
|
|
314
|
+
const instances = (layout?.propInstances ?? []).map((instance) => [instance?.id, instance?.type]);
|
|
315
|
+
const capabilitiesOf = (type) => content?.props?.types?.[type]?.capabilities ?? [];
|
|
316
|
+
;
|
|
317
|
+
(content?.npcs?.entries ?? []).forEach((npc, index) => {
|
|
318
|
+
const path5 = `$.npcs.entries[${index}]`;
|
|
319
|
+
if (!npc?.id || !npc.role || !layout?.targets?.[npc.spawn]) issues.push({ code: "invalid-npc", path: path5, message: `\u65E0\u6548\u7684 NPC\uFF1A${npc?.id ?? "\u2014"}` });
|
|
320
|
+
const shift = npc?.shift ? shiftIssue(npc.shift, "invalid-npc-shift", path5, `NPC ${npc.id} \u7684 shift`) : null;
|
|
321
|
+
if (shift) issues.push(shift);
|
|
322
|
+
});
|
|
323
|
+
;
|
|
324
|
+
(content?.lifeActivities?.entries ?? []).forEach((activity, index) => {
|
|
325
|
+
const path5 = `$.lifeActivities.entries[${index}]`;
|
|
326
|
+
if (!activity?.id || !["agent", "npc", "person"].includes(activity.participant?.kind) || !activity.steps?.length) {
|
|
327
|
+
issues.push({ code: "invalid-activity", path: path5, message: `\u65E0\u6548\u7684 Life Activity\uFF1A${activity?.id ?? "\u2014"}` });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (activity.participant?.minAgents != null && (!Number.isInteger(activity.participant.minAgents) || activity.participant.minAgents < 2)) {
|
|
331
|
+
issues.push({ code: "invalid-activity-participant", path: path5, message: `${activity.id} \u7684 minAgents \u5FC5\u987B\u662F\u81F3\u5C11 2 \u7684\u6574\u6570` });
|
|
332
|
+
}
|
|
333
|
+
const resolved = resolveActivityRequirements(activity.requires, instances, capabilitiesOf, activity.id);
|
|
334
|
+
issues.push(...resolved.issues.map((issue2) => ({ ...issue2, path: path5 })));
|
|
335
|
+
for (const step of activity.steps ?? []) {
|
|
336
|
+
if (!layout?.targets?.[step?.target] && !DYNAMIC_ACTIVITY_TARGETS.has(step?.target)) issues.push({ code: "missing-activity-target", path: path5, message: `${activity.id} \u7F3A\u5C11 Target\uFF1A${step?.target}` });
|
|
337
|
+
for (const target of step?.targets ?? []) {
|
|
338
|
+
if (!layout?.targets?.[target]) issues.push({ code: "missing-activity-target", path: path5, message: `${activity.id} \u7F3A\u5C11 Group Target\uFF1A${target}` });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
const environment = content?.environment;
|
|
343
|
+
if (!environment || !["local", "fixed"].includes(environment.clock?.mode)) {
|
|
344
|
+
issues.push({ code: "invalid-environment", path: "$.environment", message: "Environment \u7684 clock.mode \u5FC5\u987B\u662F local \u6216 fixed" });
|
|
345
|
+
} else {
|
|
346
|
+
if (environment.clock.mode === "fixed" && !validClock(environment.clock.fixedTime)) issues.push({ code: "invalid-environment", path: "$.environment.clock.fixedTime", message: "Environment \u7684 fixedTime \u65E0\u6548" });
|
|
347
|
+
if (!Array.isArray(environment.clock?.phases) || !environment.clock.phases.length) issues.push({ code: "invalid-environment", path: "$.environment.clock", message: "Environment \u7F3A\u5C11 day phases" });
|
|
348
|
+
for (const phase of environment.clock?.phases ?? []) {
|
|
349
|
+
if (!phase?.id || !validClock(phase.start)) issues.push({ code: "invalid-environment", path: "$.environment.clock.phases", message: "Environment \u5305\u542B\u65E0\u6548\u7684 day phase" });
|
|
350
|
+
}
|
|
351
|
+
if (!Array.isArray(environment.weather?.allowedConditions) || !environment.weather.allowedConditions.length) {
|
|
352
|
+
issues.push({ code: "invalid-environment", path: "$.environment.weather", message: "Environment \u7F3A\u5C11\u5929\u6C14\u7C7B\u578B" });
|
|
353
|
+
}
|
|
354
|
+
const allowedWeather = new Set(environment.weather?.allowedConditions ?? []);
|
|
355
|
+
for (const condition of [environment.weather?.condition, environment.weather?.fallback]) {
|
|
356
|
+
if (condition != null && !allowedWeather.has(condition)) {
|
|
357
|
+
issues.push({ code: "invalid-environment", path: "$.environment.weather", message: `Environment \u7684\u5929\u6C14 ${condition} \u4E0D\u5728 allowedConditions \u4E2D` });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const shift = environment.npcSchedule?.defaultShift ? shiftIssue(environment.npcSchedule.defaultShift, "invalid-environment", "$.environment.npcSchedule", "Environment \u7684 NPC \u9ED8\u8BA4\u73ED\u6B21") : null;
|
|
361
|
+
if (shift) issues.push(shift);
|
|
362
|
+
for (const [role, value] of Object.entries(environment.npcSchedule?.roleOverrides ?? {})) {
|
|
363
|
+
const roleIssue = shiftIssue(value, "invalid-environment", `$.environment.npcSchedule.roleOverrides.${role}`, `Environment \u7684 ${role} \u73ED\u6B21`);
|
|
364
|
+
if (roleIssue) issues.push(roleIssue);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return issues;
|
|
368
|
+
}
|
|
369
|
+
function graphIssueMessages(content) {
|
|
370
|
+
return graphIssues(content).map((issue2) => issue2.message);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ../src/content/validator.ts
|
|
374
|
+
function add(issues, code, path5, message) {
|
|
375
|
+
issues.push({ code, path: path5, message });
|
|
376
|
+
}
|
|
377
|
+
function validClock2(value) {
|
|
378
|
+
if (typeof value !== "string" || !/^\d{2}:\d{2}$/.test(value)) return false;
|
|
379
|
+
const [hour, minute] = value.split(":").map(Number);
|
|
380
|
+
return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59;
|
|
381
|
+
}
|
|
382
|
+
function validateShift2(value, path5, issues) {
|
|
383
|
+
if (!validClock2(value?.start) || !validClock2(value?.end)) add(issues, "invalid-shift", path5, "shift must contain valid HH:MM start and end values");
|
|
384
|
+
if (value?.endLatest !== void 0 && !validClock2(value.endLatest)) add(issues, "invalid-shift", `${path5}.endLatest`, "endLatest must be a valid HH:MM value");
|
|
385
|
+
}
|
|
386
|
+
function validateLayoutContract(layout, library, issues) {
|
|
387
|
+
const propTypeNames = [...library.props.keys()].map((id) => id.replace(/^(builtin|local)\//, ""));
|
|
388
|
+
for (const issue2 of layoutIssues(layout, propTypeNames)) add(issues, issue2.code, issue2.path, issue2.message);
|
|
389
|
+
}
|
|
390
|
+
var OFFICE_ID = /^(builtin|local)\/[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*$/;
|
|
391
|
+
var INSTANCE_ID = /^[a-z0-9][a-z0-9-]*$/;
|
|
392
|
+
var COLOR = /^#[0-9a-f]{6}$/i;
|
|
393
|
+
function validateOfficeSpec(spec, library) {
|
|
394
|
+
const issues = validateOfficeSpecShape(spec).map((entry) => ({ ...entry, code: "invalid-shape" }));
|
|
395
|
+
if (issues.length || !spec || typeof spec !== "object") return { valid: false, issues };
|
|
396
|
+
const value = spec;
|
|
397
|
+
if (!OFFICE_ID.test(value.id)) add(issues, "invalid-office-id", "$.id", "office id must use a safe builtin/ or local/ identifier");
|
|
398
|
+
if (value.origin === "official" && !value.id.startsWith("builtin/")) add(issues, "invalid-office-origin", "$.origin", "official offices require a builtin/ id");
|
|
399
|
+
if (value.origin === "custom" && !value.id.startsWith("local/")) add(issues, "invalid-office-origin", "$.origin", "custom offices require a local/ id");
|
|
400
|
+
if (!library.layouts.has(value.layout)) add(issues, "unknown-layout", "$.layout", `unknown layout ${value.layout}`);
|
|
401
|
+
if (!library.styles.has(value.style)) add(issues, "unknown-style", "$.style", `unknown style ${value.style}`);
|
|
402
|
+
if (!library.agentSkins.has(value.agentSkin)) add(issues, "unknown-agent-skin", "$.agentSkin", `unknown agent skin ${value.agentSkin}`);
|
|
403
|
+
if (!library.atmospheres.has(value.atmosphere)) add(issues, "unknown-atmosphere", "$.atmosphere", `unknown atmosphere ${value.atmosphere}`);
|
|
404
|
+
if (!library.environments.has(value.environment)) add(issues, "unknown-environment", "$.environment", `unknown environment ${value.environment}`);
|
|
405
|
+
if (value.agentProfile) {
|
|
406
|
+
if (!library.agentProfileTemplates.has(value.agentProfile.template)) add(issues, "unknown-agent-profile-template", "$.agentProfile.template", `unknown Agent Profile template ${value.agentProfile.template}`);
|
|
407
|
+
for (const [field, color] of Object.entries(value.agentProfile.appearance ?? {})) if (!COLOR.test(String(color))) add(issues, "invalid-color", `$.agentProfile.appearance.${field}`, "appearance colors must use #RRGGBB");
|
|
408
|
+
}
|
|
409
|
+
const layout = library.layouts.get(value.layout);
|
|
410
|
+
if (!layout) return { valid: false, issues };
|
|
411
|
+
validateLayoutContract(layout, library, issues);
|
|
412
|
+
for (const [id, text] of Object.entries(value.texts ?? {})) {
|
|
413
|
+
const slot = (layout.textSlots ?? []).find((entry) => entry.id === id);
|
|
414
|
+
if (!slot) add(issues, "unknown-text-area", `$.texts.${id}`, `unknown text area ${id}`);
|
|
415
|
+
else if ([...text].length > slot.maxLength) add(issues, "text-too-long", `$.texts.${id}`, `maximum ${slot.maxLength} characters`);
|
|
416
|
+
}
|
|
417
|
+
const slots = new Map((layout.placementSlots ?? []).map((slot) => [slot.id, slot]));
|
|
418
|
+
const placementIds = /* @__PURE__ */ new Set();
|
|
419
|
+
const occupiedSlots = /* @__PURE__ */ new Set();
|
|
420
|
+
for (let index = 0; index < value.placements.length; index += 1) {
|
|
421
|
+
const placement = value.placements[index];
|
|
422
|
+
const path5 = `$.placements[${index}]`;
|
|
423
|
+
if (!INSTANCE_ID.test(placement.id)) add(issues, "invalid-placement-id", `${path5}.id`, "placement id must be a safe lowercase identifier");
|
|
424
|
+
if (placementIds.has(placement.id)) add(issues, "duplicate-placement", `${path5}.id`, `duplicate placement id ${placement.id}`);
|
|
425
|
+
placementIds.add(placement.id);
|
|
426
|
+
const component = library.props.get(placement.component);
|
|
427
|
+
if (!component) add(issues, "unknown-prop", `${path5}.component`, `unknown prop ${placement.component}`);
|
|
428
|
+
const slot = slots.get(placement.slot);
|
|
429
|
+
if (!slot) add(issues, "unknown-slot", `${path5}.slot`, `unknown slot ${placement.slot}`);
|
|
430
|
+
else {
|
|
431
|
+
if (occupiedSlots.has(placement.slot)) add(issues, "occupied-slot", `${path5}.slot`, `slot ${placement.slot} is already occupied`);
|
|
432
|
+
occupiedSlots.add(placement.slot);
|
|
433
|
+
const propType = placement.component.replace(/^builtin\//, "");
|
|
434
|
+
if (component && !slot.accepts?.includes(propType)) add(issues, "incompatible-slot", path5, `${placement.component} is not accepted by ${placement.slot}`);
|
|
435
|
+
if (component && (component.size.width > slot.maxSize?.width || component.size.height > slot.maxSize?.height)) add(issues, "prop-too-large", path5, `${placement.component} exceeds ${placement.slot}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (value.placements.length > SCENE_LIMITS.props) add(issues, "too-many-props", "$.placements", `maximum ${SCENE_LIMITS.props} props`);
|
|
439
|
+
const npcIds = /* @__PURE__ */ new Set();
|
|
440
|
+
for (let index = 0; index < value.npcs.length; index += 1) {
|
|
441
|
+
const npc = value.npcs[index];
|
|
442
|
+
const path5 = `$.npcs[${index}]`;
|
|
443
|
+
if (!INSTANCE_ID.test(npc.id)) add(issues, "invalid-npc-id", `${path5}.id`, "NPC id must be a safe lowercase identifier");
|
|
444
|
+
if (npcIds.has(npc.id)) add(issues, "duplicate-npc", `${path5}.id`, `duplicate NPC id ${npc.id}`);
|
|
445
|
+
npcIds.add(npc.id);
|
|
446
|
+
const template = npc.template ? library.npcTemplates.get(npc.template) : void 0;
|
|
447
|
+
if (!npc.template || !template) add(issues, "unknown-npc-template", `${path5}.template`, `unknown NPC template ${npc.template ?? "(missing)"}`);
|
|
448
|
+
if (npc.profile && (!template?.defaultProfiles || !template.defaultProfiles.some((profile) => profile.id === npc.profile))) add(issues, "unknown-npc-profile", `${path5}.profile`, `unknown profile ${npc.profile}`);
|
|
449
|
+
if (npc.gender && !GENDER_VALUES.includes(npc.gender)) add(issues, "invalid-gender", `${path5}.gender`, `unsupported gender ${npc.gender}`);
|
|
450
|
+
if (npc.pose && !POSE_VALUES.includes(npc.pose)) add(issues, "invalid-pose", `${path5}.pose`, `unsupported pose ${npc.pose}`);
|
|
451
|
+
for (const [field, color] of Object.entries(npc.appearance ?? {})) if (!COLOR.test(String(color))) add(issues, "invalid-color", `${path5}.appearance.${field}`, "appearance colors must use #RRGGBB");
|
|
452
|
+
if (!npc.spawn || !layout.npcSpawns?.includes(npc.spawn) || !layout.targets?.[npc.spawn]) add(issues, "invalid-npc-spawn", `${path5}.spawn`, `invalid NPC spawn ${npc.spawn ?? "(missing)"}`);
|
|
453
|
+
if (npc.shift) validateShift2(npc.shift, `${path5}.shift`, issues);
|
|
454
|
+
}
|
|
455
|
+
if (value.npcs.length > SCENE_LIMITS.npcs) add(issues, "too-many-npcs", "$.npcs", `maximum ${SCENE_LIMITS.npcs} NPCs`);
|
|
456
|
+
const activities = /* @__PURE__ */ new Set();
|
|
457
|
+
const propInstances = /* @__PURE__ */ new Map();
|
|
458
|
+
const replaceableIds = new Set((layout.placementSlots ?? []).map((slot) => slot.occupiedBy).filter(Boolean));
|
|
459
|
+
for (const instance of layout.propInstances ?? []) if (!replaceableIds.has(instance.id)) propInstances.set(instance.id, instance.type);
|
|
460
|
+
for (const placement of value.placements) propInstances.set(placement.id, placement.component.replace(/^(builtin|local)\//, ""));
|
|
461
|
+
const capabilitiesOf = (type) => {
|
|
462
|
+
const prop = library.props.get(`builtin/${type}`) ?? library.props.get(`local/${type}`) ?? library.props.get(type);
|
|
463
|
+
return prop?.capabilities ?? [];
|
|
464
|
+
};
|
|
465
|
+
const preparedSlots = /* @__PURE__ */ new Map();
|
|
466
|
+
for (const slot of layout.placementSlots ?? []) {
|
|
467
|
+
const builtin = (layout.propInstances ?? []).find((instance) => instance.id === slot.occupiedBy);
|
|
468
|
+
if (!builtin) continue;
|
|
469
|
+
for (const capability of capabilitiesOf(builtin.type)) if (!preparedSlots.has(capability)) preparedSlots.set(capability, slot.id);
|
|
470
|
+
}
|
|
471
|
+
const npcRoles = new Set(value.npcs.map((npc) => library.npcTemplates.get(npc.template ?? "")?.role).filter(Boolean));
|
|
472
|
+
for (let index = 0; index < value.activities.length; index += 1) {
|
|
473
|
+
const activityId = value.activities[index];
|
|
474
|
+
const path5 = `$.activities[${index}]`;
|
|
475
|
+
if (activities.has(activityId)) add(issues, "duplicate-activity", path5, `duplicate activity ${activityId}`);
|
|
476
|
+
activities.add(activityId);
|
|
477
|
+
const recipe = library.activityRecipes.get(activityId);
|
|
478
|
+
if (!recipe) add(issues, "unknown-activity", path5, `unknown activity ${activityId}`);
|
|
479
|
+
else if (!library.activityImplementations.has(`${value.layout}|${activityId}`)) add(issues, "unsupported-activity-layout", path5, `${activityId} has no implementation for ${value.layout}`);
|
|
480
|
+
else {
|
|
481
|
+
const implementation = library.activityImplementations.get(`${value.layout}|${activityId}`);
|
|
482
|
+
const resolved = resolveActivityRequirements(implementation?.definition?.requires, propInstances, capabilitiesOf, activityId);
|
|
483
|
+
for (const issue2 of resolved.issues) add(issues, issue2.code, path5, issue2.message);
|
|
484
|
+
if (!resolved.issues.length) {
|
|
485
|
+
(implementation?.definition?.requires ?? []).forEach((requirement, requirementIndex) => {
|
|
486
|
+
const capability = requirement && typeof requirement === "object" && typeof requirement.capability === "string" ? requirement.capability : void 0;
|
|
487
|
+
const bound = resolved.bindings[requirementIndex];
|
|
488
|
+
const placement = capability && bound ? value.placements.find((entry) => entry.id === bound) : void 0;
|
|
489
|
+
if (!placement) return;
|
|
490
|
+
const prepared = preparedSlots.get(capability);
|
|
491
|
+
if (prepared === placement.slot) return;
|
|
492
|
+
add(issues, "capability-prop-slot", path5, `${placement.id} provides ${capability} from ${placement.slot}, but ${activityId} performs where this layout prepared ${capability}${prepared ? ` (${prepared})` : ""}; keep the prop where it is instead of relocating it`);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
if (!resolved.issues.length && recipe.participantKinds?.includes("npc") && recipe.participantKinds.length === 1 && recipe.participantRoles?.length && !recipe.participantRoles.some((role) => npcRoles.has(role))) {
|
|
496
|
+
add(issues, "missing-activity-participant", path5, `${activityId} has no compatible NPC in this office`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (value.activities.length > SCENE_LIMITS.activities) add(issues, "too-many-activities", "$.activities", `maximum ${SCENE_LIMITS.activities} activities`);
|
|
501
|
+
const overrides = value.environmentOverrides;
|
|
502
|
+
if (overrides?.clock?.mode === "fixed" && !validClock2(overrides.clock.fixedTime)) add(issues, "invalid-fixed-time", "$.environmentOverrides.clock.fixedTime", "fixed clock requires a valid HH:MM value");
|
|
503
|
+
if (overrides?.clock?.mode === "local" && overrides.clock.fixedTime !== void 0) add(issues, "unused-fixed-time", "$.environmentOverrides.clock.fixedTime", "local clock cannot include fixedTime");
|
|
504
|
+
if (overrides?.weather && !WEATHER_VALUES.includes(overrides.weather.fallback)) add(issues, "invalid-weather", "$.environmentOverrides.weather.fallback", "unsupported weather");
|
|
505
|
+
if (overrides?.npcSchedule?.defaultShift) validateShift2(overrides.npcSchedule.defaultShift, "$.environmentOverrides.npcSchedule.defaultShift", issues);
|
|
506
|
+
for (const [role, shift] of Object.entries(overrides?.npcSchedule?.roleOverrides ?? {})) {
|
|
507
|
+
if (![...library.npcTemplates.values()].some((template) => template.role === role)) add(issues, "unknown-npc-role", `$.environmentOverrides.npcSchedule.roleOverrides.${role}`, `unknown NPC role ${role}`);
|
|
508
|
+
validateShift2(shift, `$.environmentOverrides.npcSchedule.roleOverrides.${role}`, issues);
|
|
509
|
+
}
|
|
510
|
+
return { valid: issues.length === 0, issues };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// ../src/content/compiler.ts
|
|
514
|
+
function compileOfficeSpec(input, library) {
|
|
515
|
+
const validation = validateOfficeSpec(input, library);
|
|
516
|
+
if (!validation.valid) return { errors: validation.issues, adjustments: [] };
|
|
517
|
+
return { draft: structuredClone(input), errors: [], adjustments: [] };
|
|
518
|
+
}
|
|
519
|
+
function hash(value) {
|
|
520
|
+
let result = 2166136261;
|
|
521
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
522
|
+
result ^= value.charCodeAt(index);
|
|
523
|
+
result = Math.imul(result, 16777619);
|
|
524
|
+
}
|
|
525
|
+
return result >>> 0;
|
|
526
|
+
}
|
|
527
|
+
function upsertById(base, updates, removals, path5, adjustments, merge = (previous, update) => ({ ...previous ?? {}, ...structuredClone(update) })) {
|
|
528
|
+
const values = new Map(base.map((entry) => [entry.id, structuredClone(entry)]));
|
|
529
|
+
for (const id of removals) {
|
|
530
|
+
if (!values.delete(id)) adjustments.push({ code: "remove-missing", path: path5, message: `${id} did not exist and was ignored` });
|
|
531
|
+
}
|
|
532
|
+
for (const update of updates) values.set(update.id, merge(values.get(update.id), update));
|
|
533
|
+
return [...values.values()];
|
|
534
|
+
}
|
|
535
|
+
function truncate(values, maximum, path5, adjustments) {
|
|
536
|
+
if (values.length <= maximum) return values;
|
|
537
|
+
adjustments.push({ code: "capacity-truncated", path: path5, message: `${values.length - maximum} extra item(s) were ignored; maximum is ${maximum}` });
|
|
538
|
+
return values.slice(0, maximum);
|
|
539
|
+
}
|
|
540
|
+
function resolveNpc(npc, library, layout) {
|
|
541
|
+
const templateId = npc.template ?? library.defaultNpcTemplate;
|
|
542
|
+
const template = library.npcTemplates.get(templateId);
|
|
543
|
+
if (!template) return { ...npc, template: templateId };
|
|
544
|
+
const profiles = template.defaultProfiles ?? [];
|
|
545
|
+
const selected = npc.profile ? profiles.find((profile) => profile.id === npc.profile) : profiles.length ? profiles[hash(npc.id) % profiles.length] : void 0;
|
|
546
|
+
const role = template.role;
|
|
547
|
+
const spawnCandidates = (layout.npcSpawns ?? []).filter((spawn) => {
|
|
548
|
+
if (role === "boss") return /boss|director|manager/.test(spawn);
|
|
549
|
+
if (role === "cleaner") return /clean|service|staff|entry/.test(spawn) && !/boss/.test(spawn);
|
|
550
|
+
if (role === "receptionist") return /reception|staff|entry/.test(spawn) && !/boss/.test(spawn);
|
|
551
|
+
if (role === "secretary" || role === "attendant") return /secretary|service|staff|entry/.test(spawn) && !/boss/.test(spawn);
|
|
552
|
+
return /staff|entry/.test(spawn) && !/boss|clean|service|reception|secretary/.test(spawn);
|
|
553
|
+
});
|
|
554
|
+
const requestedSpawn = npc.spawn;
|
|
555
|
+
const requestedIsRoleSafe = requestedSpawn && (spawnCandidates.includes(requestedSpawn) || role !== "colleague");
|
|
556
|
+
const fallbackSpawn = spawnCandidates[hash(`${npc.id}:spawn`) % Math.max(1, spawnCandidates.length)] ?? layout.npcSpawns?.[hash(`${npc.id}:spawn`) % Math.max(1, layout.npcSpawns?.length ?? 0)];
|
|
557
|
+
return {
|
|
558
|
+
id: npc.id,
|
|
559
|
+
template: templateId,
|
|
560
|
+
...selected?.id ? { profile: selected.id } : {},
|
|
561
|
+
name: npc.name ?? selected?.name ?? template.name,
|
|
562
|
+
title: npc.title ?? template.defaultTitle,
|
|
563
|
+
gender: npc.gender ?? selected?.gender ?? template.defaultGender,
|
|
564
|
+
appearance: { ...template.defaultAppearance, ...selected?.appearance ?? {}, ...npc.appearance ?? {} },
|
|
565
|
+
spawn: requestedIsRoleSafe ? requestedSpawn : fallbackSpawn,
|
|
566
|
+
...npc.shift ? { shift: structuredClone(npc.shift) } : {},
|
|
567
|
+
pose: npc.pose ?? template.defaultPose
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function mergeNpc(previous, update) {
|
|
571
|
+
return {
|
|
572
|
+
...previous ?? {},
|
|
573
|
+
...structuredClone(update),
|
|
574
|
+
...previous?.appearance || update.appearance ? { appearance: { ...previous?.appearance ?? {}, ...update.appearance ?? {} } } : {},
|
|
575
|
+
...previous?.shift || update.shift ? { shift: { ...previous?.shift ?? {}, ...update.shift ?? {} } } : {}
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
function mergeEnvironment(base, patch) {
|
|
579
|
+
const clock = patch.clock ? { ...base?.clock ?? {}, ...patch.clock } : base?.clock;
|
|
580
|
+
if (clock?.mode === "local") delete clock.fixedTime;
|
|
581
|
+
const npcSchedule = patch.npcSchedule ? {
|
|
582
|
+
...base?.npcSchedule ?? {},
|
|
583
|
+
...patch.npcSchedule,
|
|
584
|
+
roleOverrides: { ...base?.npcSchedule?.roleOverrides ?? {}, ...patch.npcSchedule.roleOverrides ?? {} }
|
|
585
|
+
} : base?.npcSchedule;
|
|
586
|
+
return {
|
|
587
|
+
...clock ? { clock } : {},
|
|
588
|
+
...patch.weather || base?.weather ? { weather: { ...base?.weather ?? {}, ...patch.weather ?? {} } } : {},
|
|
589
|
+
...patch.lighting || base?.lighting ? { lighting: { ...base?.lighting ?? {}, ...patch.lighting ?? {} } } : {},
|
|
590
|
+
...npcSchedule ? { npcSchedule } : {}
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function duplicates(values) {
|
|
594
|
+
const seen = /* @__PURE__ */ new Set();
|
|
595
|
+
return values.filter((value) => seen.has(value) || !seen.add(value));
|
|
596
|
+
}
|
|
597
|
+
function patchOfficeId(base) {
|
|
598
|
+
return base.origin === "custom" ? base.id : `local/${base.id.replace(/^builtin\//, "")}`;
|
|
599
|
+
}
|
|
600
|
+
function presetIdForLocalId(localId) {
|
|
601
|
+
return localId.startsWith("local/") ? `builtin/${localId.slice("local/".length)}` : void 0;
|
|
602
|
+
}
|
|
603
|
+
function ownerPresetId(base) {
|
|
604
|
+
return base.origin === "official" ? base.id : base.basePreset;
|
|
605
|
+
}
|
|
606
|
+
function compileOfficePatch(base, patchInput, library) {
|
|
607
|
+
const shapeIssues = validateOfficePatchShape(patchInput).map((entry) => ({ ...entry, code: "invalid-patch-shape" }));
|
|
608
|
+
if (shapeIssues.length) return { errors: shapeIssues, adjustments: [] };
|
|
609
|
+
const patch = patchInput;
|
|
610
|
+
if (patch.base !== base.id) return { errors: [{ code: "base-mismatch", path: "$.base", message: `patch base ${patch.base} does not match ${base.id}` }], adjustments: [] };
|
|
611
|
+
const baseValidation = validateOfficeSpec(base, library);
|
|
612
|
+
if (!baseValidation.valid) return { errors: baseValidation.issues.map((entry) => ({ ...entry, path: `$.base${entry.path.slice(1)}` })), adjustments: [] };
|
|
613
|
+
const operationErrors = [];
|
|
614
|
+
for (const [path5, values] of [
|
|
615
|
+
["$.placements.upsert", (patch.placements?.upsert ?? []).map((entry) => entry.id)],
|
|
616
|
+
["$.placements.remove", patch.placements?.remove ?? []],
|
|
617
|
+
["$.npcs.upsert", (patch.npcs?.upsert ?? []).map((entry) => entry.id)],
|
|
618
|
+
["$.npcs.remove", patch.npcs?.remove ?? []],
|
|
619
|
+
["$.activities.enable", patch.activities?.enable ?? []],
|
|
620
|
+
["$.activities.disable", patch.activities?.disable ?? []]
|
|
621
|
+
]) {
|
|
622
|
+
for (const id of new Set(duplicates(values))) operationErrors.push({ code: "duplicate-operation", path: path5, message: `${id} appears more than once` });
|
|
623
|
+
}
|
|
624
|
+
const enabled = new Set(patch.activities?.enable ?? []);
|
|
625
|
+
for (const id of patch.activities?.disable ?? []) if (enabled.has(id)) operationErrors.push({ code: "conflicting-operation", path: "$.activities", message: `${id} cannot be enabled and disabled together` });
|
|
626
|
+
if (operationErrors.length) return { errors: operationErrors, adjustments: [] };
|
|
627
|
+
const adjustments = [];
|
|
628
|
+
const components = patch.components ?? {};
|
|
629
|
+
const layoutId = base.layout;
|
|
630
|
+
const layout = library.layouts.get(layoutId);
|
|
631
|
+
if (!layout) return { errors: [{ code: "unknown-layout", path: "$.layout", message: `unknown layout ${layoutId}` }], adjustments: [] };
|
|
632
|
+
const placements = upsertById(base.placements, patch.placements?.upsert ?? [], patch.placements?.remove ?? [], "$.placements", adjustments);
|
|
633
|
+
const npcs = upsertById(base.npcs, patch.npcs?.upsert ?? [], patch.npcs?.remove ?? [], "$.npcs", adjustments, mergeNpc);
|
|
634
|
+
const activities = new Set(base.activities);
|
|
635
|
+
for (const id of patch.activities?.disable ?? []) {
|
|
636
|
+
if (!activities.delete(id)) adjustments.push({ code: "disable-missing", path: "$.activities.disable", message: `${id} was not enabled and was ignored` });
|
|
637
|
+
}
|
|
638
|
+
for (const id of patch.activities?.enable ?? []) activities.add(id);
|
|
639
|
+
const draft = {
|
|
640
|
+
schemaVersion: OFFICE_SPEC_SCHEMA_VERSION,
|
|
641
|
+
kind: "office-spec",
|
|
642
|
+
id: patch.id ?? patchOfficeId(base),
|
|
643
|
+
name: patch.name ?? base.name,
|
|
644
|
+
origin: "custom",
|
|
645
|
+
basePreset: base.origin === "official" ? base.id : base.basePreset,
|
|
646
|
+
layout: layoutId,
|
|
647
|
+
style: components.style ?? base.style,
|
|
648
|
+
agentSkin: components.agentSkin ?? base.agentSkin,
|
|
649
|
+
placements: truncate(placements, SCENE_LIMITS.props, "$.placements", adjustments),
|
|
650
|
+
npcs: truncate(npcs, SCENE_LIMITS.npcs, "$.npcs", adjustments).map((npc) => resolveNpc(npc, library, layout)),
|
|
651
|
+
activities: truncate([...activities], SCENE_LIMITS.activities, "$.activities", adjustments),
|
|
652
|
+
atmosphere: components.atmosphere ?? base.atmosphere,
|
|
653
|
+
environment: components.environment ?? base.environment,
|
|
654
|
+
...patch.texts === null ? {} : base.texts || patch.texts ? { texts: { ...base.texts, ...patch.texts } } : {},
|
|
655
|
+
...patch.environmentOverrides === null ? {} : patch.environmentOverrides || base.environmentOverrides ? { environmentOverrides: mergeEnvironment(base.environmentOverrides, patch.environmentOverrides ?? {}) } : {},
|
|
656
|
+
...patch.agentProfile === null ? {} : patch.agentProfile || base.agentProfile ? { agentProfile: { ...base.agentProfile ?? OFFICE_SPEC_DEFAULTS.agentProfile, ...patch.agentProfile ?? {}, appearance: { ...base.agentProfile?.appearance ?? {}, ...patch.agentProfile?.appearance ?? {} } } } : {}
|
|
657
|
+
};
|
|
658
|
+
const compiled = compileOfficeSpec(draft, library);
|
|
659
|
+
return { draft: compiled.draft, errors: compiled.errors, adjustments: [...adjustments, ...compiled.adjustments] };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// ../src/creator/service.ts
|
|
663
|
+
var COMPONENT_CATEGORIES = ["summary", "room", "npcs", "props", "activities", "appearance", "environment", "all"];
|
|
664
|
+
function roomView(library, office) {
|
|
665
|
+
const layout = library.layouts.get(office.layout);
|
|
666
|
+
if (!layout) return null;
|
|
667
|
+
const occupants = /* @__PURE__ */ new Map();
|
|
668
|
+
for (const slot of layout.placementSlots ?? []) occupants.set(slot.id, null);
|
|
669
|
+
for (const placement of office.placements) occupants.set(placement.slot, placement.id);
|
|
670
|
+
return {
|
|
671
|
+
name: layout.name,
|
|
672
|
+
zones: (layout.zones ?? []).map((zone) => ({ id: zone.id, name: zone.name, x: zone.x, y: zone.y, width: zone.width, height: zone.height })),
|
|
673
|
+
slots: (layout.placementSlots ?? []).map((slot) => ({ id: slot.id, zone: slot.zone, accepts: slot.accepts ?? [], maxSize: slot.maxSize, occupiedBy: occupants.get(slot.id) ?? null })),
|
|
674
|
+
npcSpawns: layout.npcSpawns ?? [],
|
|
675
|
+
textSlots: (layout.textSlots ?? []).map((slot) => ({ id: slot.id, name: slot.name, maxLength: slot.maxLength, text: office.texts?.[slot.id] ?? slot.defaultText ?? "" })),
|
|
676
|
+
placements: office.placements.map((placement) => ({ id: placement.id, component: placement.component, slot: placement.slot, ...placement.orientation ? { orientation: placement.orientation } : {} }))
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
var CreatorService = class {
|
|
680
|
+
#registry;
|
|
681
|
+
#library;
|
|
682
|
+
constructor(registry, library) {
|
|
683
|
+
this.#registry = registry;
|
|
684
|
+
this.#library = library;
|
|
685
|
+
}
|
|
686
|
+
async listOffices() {
|
|
687
|
+
return this.#registry.list();
|
|
688
|
+
}
|
|
689
|
+
async selectOffice(id) {
|
|
690
|
+
const office = await this.#registry.get(id);
|
|
691
|
+
if (!office) return { selected: false, error: `unknown or invalid office ${id}` };
|
|
692
|
+
await this.#registry.select(id);
|
|
693
|
+
return { selected: true, office };
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Capabilities the model may map a request onto. `room` describes the room of
|
|
697
|
+
* the currently selected Office only — zones, placement slots and NPC spawns —
|
|
698
|
+
* because "add a plant" or "put a water cooler in the lounge" is only reliable
|
|
699
|
+
* when the model can see what this Office actually offers. Rooms are never
|
|
700
|
+
* presented as a choice.
|
|
701
|
+
*/
|
|
702
|
+
async listComponents(category = "summary") {
|
|
703
|
+
const office = await this.#registry.selected();
|
|
704
|
+
const full = {
|
|
705
|
+
room: roomView(this.#library, office),
|
|
706
|
+
styles: structuredClone(this.#library.descriptors.styles),
|
|
707
|
+
agentSkins: structuredClone(this.#library.descriptors.agentSkins),
|
|
708
|
+
props: structuredClone([...this.#library.props.values()]),
|
|
709
|
+
npcTemplates: structuredClone([...this.#library.npcTemplates.values()]),
|
|
710
|
+
agentProfileTemplates: structuredClone([...this.#library.agentProfileTemplates.values()]),
|
|
711
|
+
activities: [...this.#library.activityRecipes.values()].map((entry) => ({ ...structuredClone(entry), rooms: [...this.#library.activityImplementations.values()].filter((implementation) => implementation.recipe === entry.id).map((implementation) => implementation.layout) })),
|
|
712
|
+
atmospheres: structuredClone(this.#library.descriptors.atmospheres),
|
|
713
|
+
environments: structuredClone(this.#library.descriptors.environments)
|
|
714
|
+
};
|
|
715
|
+
if (category === "all") return full;
|
|
716
|
+
if (category === "room") return { room: full.room };
|
|
717
|
+
if (category === "npcs") return { npcTemplates: full.npcTemplates };
|
|
718
|
+
if (category === "props") return { room: full.room, props: full.props };
|
|
719
|
+
if (category === "activities") return { activities: full.activities };
|
|
720
|
+
if (category === "appearance") return { styles: full.styles, agentSkins: full.agentSkins, agentProfileTemplates: full.agentProfileTemplates };
|
|
721
|
+
if (category === "environment") return { atmospheres: full.atmospheres, environments: full.environments };
|
|
722
|
+
return {
|
|
723
|
+
// The compact view is also the model's edit baseline. Supplying the
|
|
724
|
+
// bounded, user-editable state here avoids filesystem inspection and
|
|
725
|
+
// repeated catalog calls just to discover an NPC id or text slot.
|
|
726
|
+
office: {
|
|
727
|
+
id: office.id,
|
|
728
|
+
name: office.name,
|
|
729
|
+
origin: office.origin,
|
|
730
|
+
agentProfile: structuredClone(office.agentProfile ?? null),
|
|
731
|
+
texts: structuredClone(office.texts ?? {}),
|
|
732
|
+
npcs: office.npcs.map((npc) => ({
|
|
733
|
+
id: npc.id,
|
|
734
|
+
template: npc.template,
|
|
735
|
+
name: npc.name,
|
|
736
|
+
title: npc.title,
|
|
737
|
+
gender: npc.gender,
|
|
738
|
+
spawn: npc.spawn
|
|
739
|
+
}))
|
|
740
|
+
},
|
|
741
|
+
counts: {
|
|
742
|
+
styles: full.styles.length,
|
|
743
|
+
agentSkins: full.agentSkins.length,
|
|
744
|
+
props: full.props.length,
|
|
745
|
+
npcTemplates: full.npcTemplates.length,
|
|
746
|
+
activities: full.activities.length,
|
|
747
|
+
atmospheres: full.atmospheres.length,
|
|
748
|
+
environments: full.environments.length
|
|
749
|
+
},
|
|
750
|
+
categories: COMPONENT_CATEGORIES.filter((entry) => entry !== "summary")
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
/** Validate, persist and select one customization without exposing draft state. */
|
|
754
|
+
async customize(patchInput, baseOffice) {
|
|
755
|
+
if (!patchInput || typeof patchInput !== "object" || Array.isArray(patchInput)) {
|
|
756
|
+
return { saved: false, errors: [{ code: "invalid-patch-shape", path: "$", message: "patch must be an object" }], adjustments: [] };
|
|
757
|
+
}
|
|
758
|
+
const base = baseOffice ? await this.#registry.get(baseOffice) : await this.#registry.selected();
|
|
759
|
+
if (!base) return { saved: false, errors: [{ code: "unknown-base", path: "$.base", message: `unknown or invalid base office ${baseOffice}` }], adjustments: [] };
|
|
760
|
+
const patch = {
|
|
761
|
+
...patchInput,
|
|
762
|
+
schemaVersion: 1,
|
|
763
|
+
kind: "office-patch",
|
|
764
|
+
base: base.id
|
|
765
|
+
};
|
|
766
|
+
const compiled = compileOfficePatch(base, patch, this.#library);
|
|
767
|
+
if (!compiled.draft) return { saved: false, errors: compiled.errors, adjustments: compiled.adjustments };
|
|
768
|
+
const owner = ownerPresetId(base);
|
|
769
|
+
const targetId = patchOfficeId(base);
|
|
770
|
+
const refuse = (code, message) => ({
|
|
771
|
+
saved: false,
|
|
772
|
+
errors: [{ code, path: "$.id", message }],
|
|
773
|
+
adjustments: compiled.adjustments
|
|
774
|
+
});
|
|
775
|
+
if (compiled.draft.id !== targetId) {
|
|
776
|
+
if (await this.#registry.get(compiled.draft.id)) {
|
|
777
|
+
return refuse("id-conflict", `${compiled.draft.id} already exists; a patch cannot overwrite another Office`);
|
|
778
|
+
}
|
|
779
|
+
const reserved = presetIdForLocalId(compiled.draft.id);
|
|
780
|
+
if (reserved && reserved !== owner && await this.#registry.get(reserved)) {
|
|
781
|
+
return refuse("reserved-office-id", `${compiled.draft.id} is the editable copy of ${reserved}`);
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
const existing = await this.#registry.get(targetId);
|
|
785
|
+
if (existing && existing.basePreset !== owner) {
|
|
786
|
+
const occupant = existing.basePreset ? `an Office based on ${existing.basePreset}` : "an Office without a base Preset";
|
|
787
|
+
return refuse("id-conflict", `${targetId} already holds ${occupant}; it will not be overwritten`);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const saved = await this.#registry.save(compiled.draft);
|
|
791
|
+
if (!saved.saved) return { saved: false, errors: saved.issues, adjustments: compiled.adjustments };
|
|
792
|
+
await this.#registry.select(compiled.draft.id);
|
|
793
|
+
return { saved: true, office: structuredClone(compiled.draft), errors: [], adjustments: compiled.adjustments };
|
|
794
|
+
}
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
// ../src/creator/commands.ts
|
|
798
|
+
function object2(value) {
|
|
799
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
800
|
+
}
|
|
801
|
+
function exact(input, fields) {
|
|
802
|
+
const allowed = /* @__PURE__ */ new Set(["command", ...fields]);
|
|
803
|
+
const unknown = Object.keys(input).filter((key) => !allowed.has(key));
|
|
804
|
+
if (unknown.length) throw new Error(`unknown command field(s): ${unknown.join(", ")}`);
|
|
805
|
+
}
|
|
806
|
+
var CreatorCommandRouter = class {
|
|
807
|
+
#creator;
|
|
808
|
+
constructor(creator) {
|
|
809
|
+
this.#creator = creator;
|
|
810
|
+
}
|
|
811
|
+
async execute(value) {
|
|
812
|
+
try {
|
|
813
|
+
if (!object2(value) || typeof value.command !== "string") throw new Error("command is required");
|
|
814
|
+
switch (value.command) {
|
|
815
|
+
case "list_offices":
|
|
816
|
+
exact(value, []);
|
|
817
|
+
return { ok: true, data: await this.#creator.listOffices() };
|
|
818
|
+
case "list_components":
|
|
819
|
+
exact(value, ["category"]);
|
|
820
|
+
if (value.category !== void 0 && !COMPONENT_CATEGORIES.includes(value.category)) throw new Error(`unknown component category ${String(value.category)}`);
|
|
821
|
+
return { ok: true, data: await this.#creator.listComponents(value.category) };
|
|
822
|
+
case "customize": {
|
|
823
|
+
exact(value, ["base", "patch"]);
|
|
824
|
+
if (value.base !== void 0 && (typeof value.base !== "string" || !value.base)) throw new Error("base must be a non-empty string");
|
|
825
|
+
const result = await this.#creator.customize(value.patch, value.base);
|
|
826
|
+
return result.saved ? { ok: true, data: result, adjustments: result.adjustments } : { ok: false, error: "Office customization was rejected; the current office was not changed", issues: result.errors, adjustments: result.adjustments };
|
|
827
|
+
}
|
|
828
|
+
default:
|
|
829
|
+
throw new Error(`unknown Creator command ${value.command}`);
|
|
830
|
+
}
|
|
831
|
+
} catch (error) {
|
|
832
|
+
return { ok: false, error: error.message };
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// ../src/creator/mode.ts
|
|
838
|
+
var CREATOR_MODE_CONTEXT = `Agent Live Creator Mode is active for this session.
|
|
839
|
+
Treat office-related natural language as a request to modify the currently selected Custom Office and use the agent_live_creator tool.
|
|
840
|
+
The currently selected Office is the only edit target. Do not inspect files, search for another copy, deliberate about replacement, invent a new Office id, or pass base/id unless the user explicitly selected another listed Office.
|
|
841
|
+
On the first edit, call list_components with the default compact summary at most once to obtain the current Agent Profile, text areas, and NPC ids. Then call customize immediately. Do not call list_offices or broader component categories unless the request genuinely needs an unknown choice.
|
|
842
|
+
For common edits, use these internal Patch shapes: agentProfile { template: "builtin/host-agent", name?, title? }; texts { company?, notice?, slogan? }; npcs { upsert: [{ id, template?, name?, title?, gender?, spawn?, pose? }], remove?: [id] }. Rename an existing NPC by its summary id. Add an ordinary colleague with a unique id and template "builtin/colleague"; omitted profile and appearance are resolved deterministically.
|
|
843
|
+
Do not expose schemas, patches, or component ids unless explicitly asked for implementation details.
|
|
844
|
+
If a request is unrelated to the office or ambiguous, do not perform it. Explain that Creator Mode is active and offer exactly these choices: continue editing, /agent-live list presets, /agent-live preset <number or name>, /agent-live custom, or /agent-live exit.
|
|
845
|
+
Map a request like "make me a police station" onto the closest complete Preset Office, then change its name, people, identities, furniture, style and activities. If the request needs a brand-new room structure (walls, areas, lanes, seats or work stations), say that it requires adding a new Office Preset and therefore a source change; never offer to swap a room in place.
|
|
846
|
+
An Office keeps its room. Moving to another room means selecting that Preset Office and editing a copy of it.
|
|
847
|
+
After every response, state that Creator Mode remains active and mention /agent-live exit.`;
|
|
848
|
+
var CreatorModeRegistry = class {
|
|
849
|
+
#sessions = /* @__PURE__ */ new Set();
|
|
850
|
+
enter(sessionId) {
|
|
851
|
+
if (!sessionId) throw new TypeError("Creator Mode requires a session id");
|
|
852
|
+
this.#sessions.add(sessionId);
|
|
853
|
+
}
|
|
854
|
+
exit(sessionId) {
|
|
855
|
+
return this.#sessions.delete(sessionId);
|
|
856
|
+
}
|
|
857
|
+
isActive(sessionId) {
|
|
858
|
+
return this.#sessions.has(sessionId);
|
|
859
|
+
}
|
|
860
|
+
clear() {
|
|
861
|
+
this.#sessions.clear();
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
|
|
865
|
+
// ../src/runtime/content-service.ts
|
|
866
|
+
import os from "node:os";
|
|
867
|
+
import path4 from "node:path";
|
|
868
|
+
import { existsSync } from "node:fs";
|
|
869
|
+
import { fileURLToPath } from "node:url";
|
|
870
|
+
|
|
871
|
+
// ../src/content/library.ts
|
|
872
|
+
import { readFile } from "node:fs/promises";
|
|
873
|
+
import path from "node:path";
|
|
874
|
+
async function readJson(file) {
|
|
875
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
876
|
+
}
|
|
877
|
+
async function loadComponentLibrary(contentRoot) {
|
|
878
|
+
const root = path.join(contentRoot, "component-library");
|
|
879
|
+
const [catalog, props, npcTemplates, agentProfileTemplates, activityRecipes, activityImplementations] = await Promise.all([
|
|
880
|
+
readJson(path.join(root, "catalog.json")),
|
|
881
|
+
readJson(path.join(root, "props.json")),
|
|
882
|
+
readJson(path.join(root, "npc-templates.json")),
|
|
883
|
+
readJson(path.join(root, "agent-profile-templates.json")),
|
|
884
|
+
readJson(path.join(root, "activity-recipes.json")),
|
|
885
|
+
readJson(path.join(root, "activity-implementations.json"))
|
|
886
|
+
]);
|
|
887
|
+
const layouts = /* @__PURE__ */ new Map();
|
|
888
|
+
for (const entry of catalog.layouts) layouts.set(entry.id, await readJson(path.join(contentRoot, "layouts", `${entry.id.replace(/^builtin\//, "")}.json`)));
|
|
889
|
+
return {
|
|
890
|
+
descriptors: {
|
|
891
|
+
styles: catalog.styles,
|
|
892
|
+
layouts: catalog.layouts,
|
|
893
|
+
agentSkins: catalog.agentSkins,
|
|
894
|
+
atmospheres: catalog.atmospheres,
|
|
895
|
+
environments: catalog.environments
|
|
896
|
+
},
|
|
897
|
+
styles: new Set(catalog.styles.map((entry) => entry.id)),
|
|
898
|
+
layouts,
|
|
899
|
+
agentSkins: new Set(catalog.agentSkins.map((entry) => entry.id)),
|
|
900
|
+
props: new Map(props.entries.map((entry) => [entry.id, entry])),
|
|
901
|
+
npcTemplates: new Map(npcTemplates.entries.map((entry) => [entry.id, entry])),
|
|
902
|
+
agentProfileTemplates: new Map(agentProfileTemplates.entries.map((entry) => [entry.id, entry])),
|
|
903
|
+
activityRecipes: new Map(activityRecipes.entries.map((entry) => [entry.id, entry])),
|
|
904
|
+
activityImplementations: new Map(activityImplementations.entries.map((entry) => [`${entry.layout}|${entry.recipe}`, entry])),
|
|
905
|
+
atmospheres: new Set(catalog.atmospheres.map((entry) => entry.id)),
|
|
906
|
+
environments: new Set(catalog.environments.map((entry) => entry.id)),
|
|
907
|
+
defaultNpcTemplate: npcTemplates.defaultTemplate,
|
|
908
|
+
npcProfilePolicy: npcTemplates.defaultInstancePolicy
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
async function loadOfficialOffices(contentRoot) {
|
|
912
|
+
const catalog = await readJson(path.join(contentRoot, "catalog.json"));
|
|
913
|
+
const publicIds = catalog.presets.filter((entry) => entry.visibility !== "internal").map((entry) => entry.id);
|
|
914
|
+
return Promise.all(publicIds.map((id) => readJson(path.join(contentRoot, "official-offices", `${id}.json`))));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// ../src/content/registry.ts
|
|
918
|
+
import { EventEmitter } from "node:events";
|
|
919
|
+
import { mkdir, readFile as readFile2, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
920
|
+
import path2 from "node:path";
|
|
921
|
+
var SAFE_LOCAL_ID = /^local\/[a-z0-9][a-z0-9-]*(?:\/[a-z0-9][a-z0-9-]*)*$/;
|
|
922
|
+
var OfficeRegistry = class {
|
|
923
|
+
root;
|
|
924
|
+
#library;
|
|
925
|
+
#official = /* @__PURE__ */ new Map();
|
|
926
|
+
#events = new EventEmitter();
|
|
927
|
+
#fallbackOffice;
|
|
928
|
+
constructor(options) {
|
|
929
|
+
this.root = path2.resolve(options.root);
|
|
930
|
+
this.#library = options.library;
|
|
931
|
+
for (const office of options.officialOffices) this.#official.set(office.id, structuredClone(office));
|
|
932
|
+
this.#fallbackOffice = options.fallbackOffice ?? "builtin/tech-open-office";
|
|
933
|
+
if (!this.#official.has(this.#fallbackOffice)) throw new Error(`unknown fallback office ${this.#fallbackOffice}`);
|
|
934
|
+
}
|
|
935
|
+
onChange(listener) {
|
|
936
|
+
this.#events.on("change", listener);
|
|
937
|
+
return () => this.#events.off("change", listener);
|
|
938
|
+
}
|
|
939
|
+
async initialize() {
|
|
940
|
+
await mkdir(this.#officeDir(), { recursive: true });
|
|
941
|
+
}
|
|
942
|
+
async list() {
|
|
943
|
+
await this.initialize();
|
|
944
|
+
const selected = await this.selectedId();
|
|
945
|
+
const entries = [...this.#official.values()].map((office) => ({ id: office.id, name: office.name, origin: "official", selected: office.id === selected }));
|
|
946
|
+
for (const file of await readdir(this.#officeDir())) {
|
|
947
|
+
if (!file.endsWith(".json")) continue;
|
|
948
|
+
try {
|
|
949
|
+
const office = await this.#readCustomFile(path2.join(this.#officeDir(), file));
|
|
950
|
+
entries.push({ id: office.id, name: office.name, origin: "custom", selected: office.id === selected });
|
|
951
|
+
} catch (error) {
|
|
952
|
+
console.warn(`Agent Live ignored invalid custom office ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return [
|
|
956
|
+
...entries.filter((entry) => entry.origin === "official"),
|
|
957
|
+
...entries.filter((entry) => entry.origin === "custom").sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id))
|
|
958
|
+
];
|
|
959
|
+
}
|
|
960
|
+
async get(id) {
|
|
961
|
+
const official = this.#official.get(id);
|
|
962
|
+
if (official) return structuredClone(official);
|
|
963
|
+
if (!SAFE_LOCAL_ID.test(id)) return void 0;
|
|
964
|
+
try {
|
|
965
|
+
return await this.#readCustomFile(this.#fileFor(id));
|
|
966
|
+
} catch {
|
|
967
|
+
return void 0;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
async save(spec) {
|
|
971
|
+
const validation = validateOfficeSpec(spec, this.#library);
|
|
972
|
+
if (!validation.valid) return { saved: false, issues: validation.issues };
|
|
973
|
+
if (spec.origin !== "custom" || !SAFE_LOCAL_ID.test(spec.id)) return { saved: false, issues: [{ code: "official-read-only", path: "$.id", message: "only local/ custom offices can be saved" }] };
|
|
974
|
+
await this.initialize();
|
|
975
|
+
const destination = this.#fileFor(spec.id);
|
|
976
|
+
const temporary = `${destination}.${process.pid}.${Date.now()}.tmp`;
|
|
977
|
+
await writeFile(temporary, `${JSON.stringify(spec, null, " ")}
|
|
978
|
+
`, { encoding: "utf8", mode: 384 });
|
|
979
|
+
await rename(temporary, destination);
|
|
980
|
+
this.#events.emit("change", spec.id);
|
|
981
|
+
return { saved: true, issues: [] };
|
|
982
|
+
}
|
|
983
|
+
async remove(id) {
|
|
984
|
+
if (!SAFE_LOCAL_ID.test(id)) return false;
|
|
985
|
+
const selected = await this.selectedId();
|
|
986
|
+
try {
|
|
987
|
+
await rm(this.#fileFor(id));
|
|
988
|
+
} catch (error) {
|
|
989
|
+
if (error?.code === "ENOENT") return false;
|
|
990
|
+
throw error;
|
|
991
|
+
}
|
|
992
|
+
if (selected === id) await this.select(this.#fallbackOffice);
|
|
993
|
+
this.#events.emit("change", id);
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
async select(id) {
|
|
997
|
+
if (!await this.get(id)) throw new Error(`unknown or invalid office ${id}`);
|
|
998
|
+
await this.initialize();
|
|
999
|
+
const state = { schemaVersion: 1, selectedOffice: id };
|
|
1000
|
+
const destination = this.#stateFile();
|
|
1001
|
+
const temporary = `${destination}.${process.pid}.${Date.now()}.tmp`;
|
|
1002
|
+
await writeFile(temporary, `${JSON.stringify(state, null, " ")}
|
|
1003
|
+
`, { encoding: "utf8", mode: 384 });
|
|
1004
|
+
await rename(temporary, destination);
|
|
1005
|
+
this.#events.emit("change", id);
|
|
1006
|
+
}
|
|
1007
|
+
async selectedId() {
|
|
1008
|
+
try {
|
|
1009
|
+
const state = JSON.parse(await readFile2(this.#stateFile(), "utf8"));
|
|
1010
|
+
if (state.schemaVersion === 1 && state.selectedOffice && await this.get(state.selectedOffice)) return state.selectedOffice;
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
return this.#fallbackOffice;
|
|
1014
|
+
}
|
|
1015
|
+
async selected() {
|
|
1016
|
+
return await this.get(await this.selectedId()) ?? structuredClone(this.#official.get(this.#fallbackOffice));
|
|
1017
|
+
}
|
|
1018
|
+
#officeDir() {
|
|
1019
|
+
return path2.join(this.root, "offices");
|
|
1020
|
+
}
|
|
1021
|
+
#stateFile() {
|
|
1022
|
+
return path2.join(this.root, "registry.json");
|
|
1023
|
+
}
|
|
1024
|
+
#fileFor(id) {
|
|
1025
|
+
return path2.join(this.#officeDir(), `${encodeURIComponent(id.slice("local/".length))}.json`);
|
|
1026
|
+
}
|
|
1027
|
+
async #readCustomFile(file) {
|
|
1028
|
+
const spec = JSON.parse(await readFile2(file, "utf8"));
|
|
1029
|
+
const validation = validateOfficeSpec(spec, this.#library);
|
|
1030
|
+
if (!validation.valid || spec.origin !== "custom" || !SAFE_LOCAL_ID.test(spec.id)) throw new Error(`invalid custom office ${file}`);
|
|
1031
|
+
if (this.#fileFor(spec.id) !== file) throw new Error(`custom office filename does not match id ${spec.id}`);
|
|
1032
|
+
return spec;
|
|
1033
|
+
}
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
// ../src/content/runtime-content.ts
|
|
1037
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1038
|
+
import path3 from "node:path";
|
|
1039
|
+
var readJson2 = async (file) => JSON.parse(await readFile3(file, "utf8"));
|
|
1040
|
+
var short = (id) => id.replace(/^builtin\//, "");
|
|
1041
|
+
async function asset(contentRoot, family, id, suffix = "") {
|
|
1042
|
+
const name = suffix && short(id).endsWith(suffix) ? short(id).slice(0, -suffix.length) : short(id);
|
|
1043
|
+
return readJson2(path3.join(contentRoot, family, `${name}.json`));
|
|
1044
|
+
}
|
|
1045
|
+
async function resolveRuntimeContent(spec, contentRoot, library) {
|
|
1046
|
+
const layout = structuredClone(library.layouts.get(spec.layout));
|
|
1047
|
+
if (!layout) throw new Error(`unknown layout ${spec.layout}`);
|
|
1048
|
+
layout.textSlots = (layout.textSlots ?? []).map((slot) => ({ ...slot, text: spec.texts?.[slot.id] ?? slot.defaultText ?? "" }));
|
|
1049
|
+
const officialId = short(spec.layout);
|
|
1050
|
+
const scaffold = await readJson2(path3.join(contentRoot, "presets", `${officialId}.json`));
|
|
1051
|
+
const [style, agentSkin, atmosphere, environment] = await Promise.all([
|
|
1052
|
+
asset(contentRoot, "styles", spec.style),
|
|
1053
|
+
asset(contentRoot, "agent-skins", spec.agentSkin),
|
|
1054
|
+
asset(contentRoot, "atmospheres", spec.atmosphere, "-atmosphere"),
|
|
1055
|
+
asset(contentRoot, "environments", spec.environment, "-environment")
|
|
1056
|
+
]);
|
|
1057
|
+
const slottedIds = new Set((layout.placementSlots ?? []).map((slot) => slot.occupiedBy).filter(Boolean));
|
|
1058
|
+
for (const placement of spec.placements) slottedIds.add(placement.id);
|
|
1059
|
+
layout.propInstances = (layout.propInstances ?? []).filter((entry) => !slottedIds.has(entry.id));
|
|
1060
|
+
for (const placement of spec.placements) {
|
|
1061
|
+
const slot = layout.placementSlots.find((entry) => entry.id === placement.slot);
|
|
1062
|
+
layout.propInstances.push({ id: placement.id, type: short(placement.component), x: slot.x, y: slot.y, ...placement.orientation ? { orientation: placement.orientation } : {} });
|
|
1063
|
+
}
|
|
1064
|
+
const propTypes = Object.fromEntries([...library.props.values()].map((entry) => [short(entry.id), { size: entry.size, capabilities: entry.capabilities, renderer: entry.renderer }]));
|
|
1065
|
+
const props = { schemaVersion: 1, kind: "props", id: `local/${short(spec.id)}-props`, name: `${spec.name} props`, version: "1.0.0", contract: "single-office-v1", types: propTypes };
|
|
1066
|
+
const npcs = {
|
|
1067
|
+
schemaVersion: 1,
|
|
1068
|
+
kind: "npcs",
|
|
1069
|
+
id: `local/${short(spec.id)}-npcs`,
|
|
1070
|
+
name: `${spec.name} NPCs`,
|
|
1071
|
+
version: "1.0.0",
|
|
1072
|
+
contract: "single-office-v1",
|
|
1073
|
+
entries: spec.npcs.map((npc) => ({ ...structuredClone(npc), role: library.npcTemplates.get(npc.template)?.role }))
|
|
1074
|
+
};
|
|
1075
|
+
const capabilitiesOf = (type) => propTypes[type]?.capabilities ?? [];
|
|
1076
|
+
const activityInstances = (layout.propInstances ?? []).map((entry) => [entry.id, entry.type]);
|
|
1077
|
+
const activityIds = [...spec.activities];
|
|
1078
|
+
const entries = activityIds.map((id) => {
|
|
1079
|
+
const implementation = library.activityImplementations.get(`${spec.layout}|${id}`);
|
|
1080
|
+
if (!implementation) throw new Error(`activity ${id} has no implementation compatible with ${spec.layout}`);
|
|
1081
|
+
const definition = structuredClone(implementation.definition);
|
|
1082
|
+
return { ...definition, bindings: resolveActivityRequirements(definition.requires, activityInstances, capabilitiesOf, id).bindings };
|
|
1083
|
+
});
|
|
1084
|
+
const lifeActivities = { schemaVersion: 1, kind: "life-activities", id: `local/${short(spec.id)}-activities`, name: `${spec.name} activities`, version: "1.0.0", contract: "single-office-v1", entries };
|
|
1085
|
+
const agentProfileTemplate = library.agentProfileTemplates.get(spec.agentProfile?.template ?? "builtin/host-agent");
|
|
1086
|
+
const agentProfile = {
|
|
1087
|
+
template: agentProfileTemplate?.id ?? "builtin/host-agent",
|
|
1088
|
+
...spec.agentProfile?.name ? { name: spec.agentProfile.name } : {},
|
|
1089
|
+
...spec.agentProfile?.title ? { title: spec.agentProfile.title } : {},
|
|
1090
|
+
appearance: { ...agentProfileTemplate?.defaultAppearance ?? {}, ...spec.agentProfile?.appearance ?? {} }
|
|
1091
|
+
};
|
|
1092
|
+
if (spec.environmentOverrides?.clock) environment.clock = { ...environment.clock, ...spec.environmentOverrides.clock };
|
|
1093
|
+
if (spec.environmentOverrides?.weather) environment.weather = { ...environment.weather, ...spec.environmentOverrides.weather };
|
|
1094
|
+
if (spec.environmentOverrides?.lighting) environment.lighting = { ...environment.lighting, enabled: spec.environmentOverrides.lighting.auto };
|
|
1095
|
+
if (spec.environmentOverrides?.npcSchedule) environment.npcSchedule = {
|
|
1096
|
+
...environment.npcSchedule,
|
|
1097
|
+
...spec.environmentOverrides.npcSchedule,
|
|
1098
|
+
roleOverrides: { ...environment.npcSchedule?.roleOverrides ?? {}, ...spec.environmentOverrides.npcSchedule.roleOverrides ?? {} }
|
|
1099
|
+
};
|
|
1100
|
+
const preset = { ...scaffold, id: spec.id, name: spec.name, officeSpec: spec.id };
|
|
1101
|
+
delete preset.content;
|
|
1102
|
+
return {
|
|
1103
|
+
preset,
|
|
1104
|
+
style,
|
|
1105
|
+
layout,
|
|
1106
|
+
agentSkin,
|
|
1107
|
+
agentProfile,
|
|
1108
|
+
props,
|
|
1109
|
+
npcs,
|
|
1110
|
+
lifeActivities,
|
|
1111
|
+
atmosphere,
|
|
1112
|
+
environment
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// ../src/runtime/content-service.ts
|
|
1117
|
+
function pluginRoot() {
|
|
1118
|
+
return path4.resolve(path4.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
1119
|
+
}
|
|
1120
|
+
function bundledContentRoot() {
|
|
1121
|
+
const directory = path4.dirname(fileURLToPath(import.meta.url));
|
|
1122
|
+
const candidates = [
|
|
1123
|
+
path4.resolve(directory, "../../web/v2/content"),
|
|
1124
|
+
path4.resolve(directory, "../web/v2/content")
|
|
1125
|
+
];
|
|
1126
|
+
return candidates.find((candidate) => existsSync(candidate)) ?? path4.join(pluginRoot(), "web/v2/content");
|
|
1127
|
+
}
|
|
1128
|
+
var OfficeContentService = class _OfficeContentService {
|
|
1129
|
+
registry;
|
|
1130
|
+
#contentRoot;
|
|
1131
|
+
library;
|
|
1132
|
+
constructor(contentRoot, library, registry) {
|
|
1133
|
+
this.#contentRoot = contentRoot;
|
|
1134
|
+
this.library = library;
|
|
1135
|
+
this.registry = registry;
|
|
1136
|
+
}
|
|
1137
|
+
static async create(options = {}) {
|
|
1138
|
+
const contentRoot = options.contentRoot ?? bundledContentRoot();
|
|
1139
|
+
const library = await loadComponentLibrary(contentRoot);
|
|
1140
|
+
const officialOffices = await loadOfficialOffices(contentRoot);
|
|
1141
|
+
const dataRoot = options.dataRoot ?? process.env.AGENT_LIVE_DATA_DIR ?? path4.join(os.homedir(), ".agent-live");
|
|
1142
|
+
const registry = new OfficeRegistry({ root: dataRoot, library, officialOffices });
|
|
1143
|
+
return new _OfficeContentService(contentRoot, library, registry);
|
|
1144
|
+
}
|
|
1145
|
+
list() {
|
|
1146
|
+
return this.registry.list();
|
|
1147
|
+
}
|
|
1148
|
+
select(id) {
|
|
1149
|
+
return this.registry.select(id);
|
|
1150
|
+
}
|
|
1151
|
+
async resolve(id) {
|
|
1152
|
+
const office = id ? await this.registry.get(id) : await this.registry.selected();
|
|
1153
|
+
if (!office) throw new Error(`unknown or invalid office ${id}`);
|
|
1154
|
+
const compiled = compileOfficeSpec(office, this.library);
|
|
1155
|
+
if (!compiled.draft) throw new Error(`office ${id} failed compilation: ${compiled.errors.map((issue2) => issue2.message).join("; ")}`);
|
|
1156
|
+
const graph = await resolveRuntimeContent(compiled.draft, this.#contentRoot, this.library);
|
|
1157
|
+
const issues = graphIssueMessages(graph);
|
|
1158
|
+
if (issues.length) throw new Error(`office ${office.id} produced content the viewer cannot render: ${issues.join("; ")}`);
|
|
1159
|
+
return graph;
|
|
1160
|
+
}
|
|
1161
|
+
subscribe(listener) {
|
|
1162
|
+
const stopRegistry = this.registry.onChange((officeId) => listener({ type: "office", officeId }));
|
|
1163
|
+
return stopRegistry;
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
// src/creator.ts
|
|
1168
|
+
var commandOfficeProjections = /* @__PURE__ */ new Map();
|
|
1169
|
+
var currentOfficeProjection = null;
|
|
1170
|
+
function sessionEventContent(value) {
|
|
1171
|
+
return JSON.parse(JSON.stringify(value));
|
|
1172
|
+
}
|
|
1173
|
+
function officeProjection(value) {
|
|
1174
|
+
if (!value || typeof value !== "object") return null;
|
|
1175
|
+
const projection = value.officeProjection;
|
|
1176
|
+
if (!projection || typeof projection !== "object") return null;
|
|
1177
|
+
const candidate = projection;
|
|
1178
|
+
return typeof candidate.revision === "number" && "content" in candidate ? { revision: candidate.revision, content: candidate.content } : null;
|
|
1179
|
+
}
|
|
1180
|
+
function modelResult(value) {
|
|
1181
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return value;
|
|
1182
|
+
const { officeProjection: _officeProjection, ...visible } = value;
|
|
1183
|
+
return visible;
|
|
1184
|
+
}
|
|
1185
|
+
var SKILL = `# Agent Live Creator
|
|
1186
|
+
|
|
1187
|
+
Use the agent_live_creator tool when Agent Live Creator Mode is active. The user enters with /agent-live custom and exits with /agent-live exit. There is no draft, preview, confirmation, save, undo, or discard step; every valid customization applies atomically.
|
|
1188
|
+
|
|
1189
|
+
For common changes, call customize directly. Inspect list_offices or the narrowest list_components category only when a choice is unknown; list_components defaults to a compact summary, and all is reserved for an explicit complete-catalog request. Omit base to modify the currently selected Office, or provide an Office id to start from that Office. Customize validates, saves, selects, and immediately displays the result.
|
|
1190
|
+
|
|
1191
|
+
The currently selected Office is authoritative. Never inspect files, search for a similarly based Custom Office, deliberate about replacement, invent a new Office id, or pass base/id unless the user explicitly selected another listed Office. On the first edit, call the default compact list_components summary at most once to obtain the current Agent Profile, text areas, and NPC ids, then call customize immediately. Do not call list_offices for an ordinary edit.
|
|
1192
|
+
|
|
1193
|
+
For common edits, use these internal Patch shapes: agentProfile { template: "builtin/host-agent", name?, title? }; texts { company?, notice?, slogan? }; npcs { upsert: [{ id, template?, name?, title?, gender?, spawn?, pose? }], remove?: [id] }. Rename an existing NPC by its compact-summary id. Add an ordinary colleague with a unique id and template "builtin/colleague"; omitted profile and appearance are resolved deterministically.
|
|
1194
|
+
|
|
1195
|
+
Never expose internal component ids, schemas, or patches unless the user explicitly asks for implementation details. Map unsupported input to the closest supported capability without interrupting generation, then summarize defaults, substitutions, ignored requests, and source-code-only requests after applying the change.
|
|
1196
|
+
|
|
1197
|
+
An Office keeps its room. Map a request like "make me a police station" onto the closest complete Preset Office, then change its name, people, identities, furniture, style and activities. A brand-new room structure needs a new Office Preset, which is a source change \u2014 say so instead of swapping a room in place.
|
|
1198
|
+
|
|
1199
|
+
The public commands are exactly: /agent-live list presets, /agent-live preset <number or exact name>, /agent-live custom, /agent-live exit.`;
|
|
1200
|
+
function commandPayload(operation, args) {
|
|
1201
|
+
switch (operation) {
|
|
1202
|
+
case "list_offices":
|
|
1203
|
+
case "list_components":
|
|
1204
|
+
return { command: operation, ...args.category ? { category: args.category } : {} };
|
|
1205
|
+
case "customize":
|
|
1206
|
+
return { command: operation, ...args.base ? { base: args.base } : {}, patch: args.patch };
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
async function registerCreator(ctx) {
|
|
1210
|
+
const content = await OfficeContentService.create();
|
|
1211
|
+
const service = new CreatorService(content.registry, content.library);
|
|
1212
|
+
const router = new CreatorCommandRouter(service);
|
|
1213
|
+
const modes = new CreatorModeRegistry();
|
|
1214
|
+
let selectedOfficeProjection = {
|
|
1215
|
+
revision: Date.now(),
|
|
1216
|
+
content: sessionEventContent(await content.resolve())
|
|
1217
|
+
};
|
|
1218
|
+
currentOfficeProjection = selectedOfficeProjection;
|
|
1219
|
+
ctx.on("session/disposed", (session) => {
|
|
1220
|
+
modes.exit(String(session.id));
|
|
1221
|
+
});
|
|
1222
|
+
ctx.systemPrompt.context({
|
|
1223
|
+
name: "agent-live-creator-mode",
|
|
1224
|
+
order: ctx.systemPrompt.getContextOrder("SUBAGENT_DELEGATION") + 10,
|
|
1225
|
+
text: (assembly) => assembly.agent && modes.isActive(String(assembly.agent.id)) ? CREATOR_MODE_CONTEXT : ""
|
|
1226
|
+
});
|
|
1227
|
+
const officeProjectionSchema = z.unknown();
|
|
1228
|
+
ctx.sessionProjections.register({
|
|
1229
|
+
key: "agentLiveOffice",
|
|
1230
|
+
stateSchema: officeProjectionSchema,
|
|
1231
|
+
init: () => currentOfficeProjection ?? selectedOfficeProjection,
|
|
1232
|
+
apply: (state, event) => {
|
|
1233
|
+
if (event.type === "command/done") {
|
|
1234
|
+
const next2 = commandOfficeProjections.get(String(event.data.commandId));
|
|
1235
|
+
if (!next2) return state;
|
|
1236
|
+
commandOfficeProjections.delete(String(event.data.commandId));
|
|
1237
|
+
return next2;
|
|
1238
|
+
}
|
|
1239
|
+
if (event.type !== "tool/result") return state;
|
|
1240
|
+
const meta = event.data.meta;
|
|
1241
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) return state;
|
|
1242
|
+
const next = meta.agentLiveOffice;
|
|
1243
|
+
return next && typeof next === "object" ? next : state;
|
|
1244
|
+
},
|
|
1245
|
+
wire: { viewSchema: officeProjectionSchema, view: (state) => state },
|
|
1246
|
+
stateVersion: 3
|
|
1247
|
+
});
|
|
1248
|
+
ctx.skills.register({
|
|
1249
|
+
name: "agent-live-creator",
|
|
1250
|
+
description: "Create or modify a local Agent Live office from natural language.",
|
|
1251
|
+
whenToUse: "Use for Agent Live office customization, presets, NPCs, furniture, visual style, environment, schedules, or agent identity.",
|
|
1252
|
+
content: SKILL,
|
|
1253
|
+
source: "bundled"
|
|
1254
|
+
});
|
|
1255
|
+
ctx.commands.register({
|
|
1256
|
+
name: "agent-live",
|
|
1257
|
+
description: "Enter or exit Creator Mode, or inspect available Offices.",
|
|
1258
|
+
input: { hint: "custom | exit | list presets | preset <number/name>" },
|
|
1259
|
+
async handler(invocation) {
|
|
1260
|
+
const rawInput = invocation.rawInput.trim().replace(/\s+/g, " ");
|
|
1261
|
+
const input = rawInput.toLowerCase();
|
|
1262
|
+
const sessionId = String(invocation.agent.id);
|
|
1263
|
+
if (input === "custom") {
|
|
1264
|
+
modes.enter(sessionId);
|
|
1265
|
+
return { kind: "success", text: "Creator Mode is active. Describe an office change, or use /agent-live exit to leave." };
|
|
1266
|
+
}
|
|
1267
|
+
if (input === "exit") {
|
|
1268
|
+
return { kind: "success", text: modes.exit(sessionId) ? "Creator Mode exited." : "Creator Mode was not active." };
|
|
1269
|
+
}
|
|
1270
|
+
if (input === "list preset" || input === "list presets") {
|
|
1271
|
+
const offices = await service.listOffices();
|
|
1272
|
+
const lines = offices.map((office, index) => `${index + 1}. ${office.name}${office.selected ? " (selected)" : ""}
|
|
1273
|
+
/agent-live preset ${index + 1}`);
|
|
1274
|
+
const officialCount = offices.filter((office) => office.origin === "official").length;
|
|
1275
|
+
lines.splice(officialCount, 0, ...officialCount < offices.length ? ["", "Custom Offices:"] : []);
|
|
1276
|
+
lines.unshift("Preset Offices:");
|
|
1277
|
+
return { kind: "success", text: lines.join("\n") + "\n\nSelect with /agent-live preset <number or name>, then edit it with /agent-live custom." };
|
|
1278
|
+
}
|
|
1279
|
+
if (input.startsWith("preset ")) {
|
|
1280
|
+
const selector = rawInput.slice(rawInput.indexOf(" ") + 1).trim();
|
|
1281
|
+
const offices = await service.listOffices();
|
|
1282
|
+
const index = /^\d+$/.test(selector) ? Number(selector) - 1 : -1;
|
|
1283
|
+
const matches = offices.filter((entry) => entry.name.toLowerCase() === selector.toLowerCase());
|
|
1284
|
+
const office = index >= 0 ? offices[index] : matches.length === 1 ? matches[0] : void 0;
|
|
1285
|
+
if (!office) return { kind: "error", text: `Unknown preset "${selector}". Use /agent-live list presets to see the available choices.` };
|
|
1286
|
+
const result = await service.selectOffice(office.id);
|
|
1287
|
+
if (!result.selected) return { kind: "error", text: result.error };
|
|
1288
|
+
selectedOfficeProjection = { revision: Date.now(), content: sessionEventContent(await content.resolve(office.id)) };
|
|
1289
|
+
currentOfficeProjection = selectedOfficeProjection;
|
|
1290
|
+
commandOfficeProjections.set(String(invocation.commandId), selectedOfficeProjection);
|
|
1291
|
+
modes.exit(sessionId);
|
|
1292
|
+
return { kind: "success", text: `Selected ${office.name}. Agent Live has updated.` };
|
|
1293
|
+
}
|
|
1294
|
+
if (input) return { kind: "error", text: "Use /agent-live custom, /agent-live exit, /agent-live list presets, or /agent-live preset <number or name>." };
|
|
1295
|
+
return { kind: "success", text: modes.isActive(sessionId) ? "Creator Mode is active. Use /agent-live exit to leave." : "Use /agent-live custom to start editing the office." };
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
ctx.tools.register(defineTool({
|
|
1299
|
+
name: "agent_live_creator",
|
|
1300
|
+
description: "Inspect capabilities or directly validate, save, select, and display an Agent Live office customization.",
|
|
1301
|
+
parameters: {
|
|
1302
|
+
operation: { type: "string", required: true, enum: ["list_offices", "list_components", "customize"] },
|
|
1303
|
+
category: { type: "string", enum: ["summary", "room", "npcs", "props", "activities", "appearance", "environment", "all"], description: "Narrow component query; defaults to a compact summary." },
|
|
1304
|
+
base: { type: "string", description: "Optional Office id to use as the base; defaults to the selected Office." },
|
|
1305
|
+
patch: { type: "json", description: "Requested changes expressed with the bounded Office Patch fields; metadata is filled internally." }
|
|
1306
|
+
},
|
|
1307
|
+
output: {
|
|
1308
|
+
schema: { type: "json" },
|
|
1309
|
+
render: (_args, value) => [{ type: "text", text: JSON.stringify(modelResult(value)) }],
|
|
1310
|
+
presentationMeta: (_args, value) => {
|
|
1311
|
+
const projection = officeProjection(value);
|
|
1312
|
+
return projection ? { agentLiveOffice: projection } : {};
|
|
1313
|
+
}
|
|
1314
|
+
},
|
|
1315
|
+
async execute(args, exec) {
|
|
1316
|
+
const result = await router.execute(commandPayload(args.operation, args));
|
|
1317
|
+
let projection = null;
|
|
1318
|
+
if (result.ok && args.operation === "customize") {
|
|
1319
|
+
const customized = result.data;
|
|
1320
|
+
if (customized.office?.id) {
|
|
1321
|
+
projection = { revision: Date.now(), content: sessionEventContent(await content.resolve(customized.office.id)) };
|
|
1322
|
+
selectedOfficeProjection = projection;
|
|
1323
|
+
currentOfficeProjection = projection;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return {
|
|
1327
|
+
...result,
|
|
1328
|
+
...projection ? { officeProjection: projection } : {}
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
}));
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/index.ts
|
|
1335
|
+
var inject = ["commands", "sessions", "sessionProjections", "skills", "systemPrompt", "tools"];
|
|
1336
|
+
async function apply(ctx) {
|
|
1337
|
+
await registerCreator(ctx);
|
|
1338
|
+
}
|
|
1339
|
+
export {
|
|
1340
|
+
apply,
|
|
1341
|
+
inject
|
|
1342
|
+
};
|