silvermoon 0.1.3 → 0.2.0-rc.1
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 +40 -24
- package/README.zh-CN.md +36 -24
- package/docs/core-concepts.md +28 -11
- package/docs/getting-started.md +41 -9
- package/docs/npm-package-releases.md +8 -7
- package/docs/operations.md +60 -23
- package/docs/reference.md +125 -67
- package/docs/repository-tasks.md +29 -23
- package/package.json +3 -3
- package/schema/v1/command-report.schema.json +178 -0
- package/schema/v1/domain-message.schema.json +278 -0
- package/schema/v2/trace-event.schema.json +122 -0
- package/skills/silvermoon/SKILL.md +36 -25
- package/skills/silvermoon/references/adoption.md +8 -9
- package/src/cli.js +63 -9
- package/src/create-idea.js +260 -193
- package/src/dialogue.js +6 -305
- package/src/domain.js +592 -0
- package/src/idea-query.js +230 -0
- package/src/ideas.js +1 -0
- package/src/index.js +76 -31
- package/src/language.js +8 -0
- package/src/list-ideas.js +133 -0
- package/src/response.js +443 -0
- package/src/trace.js +90 -4
- package/src/whatsnext.js +81 -84
package/src/response.js
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_LANGUAGE,
|
|
3
|
+
localize,
|
|
4
|
+
resolveOutputLanguage,
|
|
5
|
+
} from "./language.js";
|
|
6
|
+
|
|
7
|
+
const BLOCKED_STATES = new Set([
|
|
8
|
+
"check-unavailable",
|
|
9
|
+
"idea-create-failed",
|
|
10
|
+
"phase-guidance-invalid",
|
|
11
|
+
"project-setup-required",
|
|
12
|
+
"repository-preparation-required",
|
|
13
|
+
"repository-sync-required",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function clone(value) {
|
|
17
|
+
return value === undefined ? undefined : structuredClone(value);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function responseLanguage(intention, observation) {
|
|
21
|
+
return observation.outputLanguage
|
|
22
|
+
?? resolveOutputLanguage({
|
|
23
|
+
content: observation.configuration?.preferredLanguage,
|
|
24
|
+
override: intention.args?.language ?? undefined,
|
|
25
|
+
}).tag
|
|
26
|
+
?? DEFAULT_LANGUAGE;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeNextSteps(nextSteps) {
|
|
30
|
+
if (nextSteps === undefined || nextSteps === null || nextSteps === "") {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const values = Array.isArray(nextSteps) ? nextSteps : [nextSteps];
|
|
34
|
+
return values.map((step) =>
|
|
35
|
+
typeof step === "string"
|
|
36
|
+
? { type: "instruction", text: step }
|
|
37
|
+
: clone(step)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function observationDetails(observation) {
|
|
42
|
+
const details = {};
|
|
43
|
+
if (observation.root !== undefined) details.root = observation.root;
|
|
44
|
+
if (observation.version !== undefined) details.version = clone(observation.version);
|
|
45
|
+
if (observation.configuration !== undefined) {
|
|
46
|
+
details.primary = {
|
|
47
|
+
repository: observation.configuration.primaryRepository,
|
|
48
|
+
branch: observation.configuration.primaryBranch,
|
|
49
|
+
};
|
|
50
|
+
details.contentLanguage = observation.configuration.preferredLanguage;
|
|
51
|
+
}
|
|
52
|
+
return details;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function blockedSummary(command, observation, language) {
|
|
56
|
+
const summaries = {
|
|
57
|
+
"check-unavailable": localize(
|
|
58
|
+
language,
|
|
59
|
+
"The requested repository snapshot could not be validated.",
|
|
60
|
+
"无法验证请求的 repository snapshot。",
|
|
61
|
+
),
|
|
62
|
+
"idea-create-failed": localize(
|
|
63
|
+
language,
|
|
64
|
+
"The repository was ready, but creating the idea failed.",
|
|
65
|
+
"repository 已就绪,但创建 idea 失败。",
|
|
66
|
+
),
|
|
67
|
+
"phase-guidance-invalid": localize(
|
|
68
|
+
language,
|
|
69
|
+
"The current phase guidance is invalid.",
|
|
70
|
+
"当前阶段 guidance 无效。",
|
|
71
|
+
),
|
|
72
|
+
"project-setup-required": localize(
|
|
73
|
+
language,
|
|
74
|
+
"The project requires Silvermoon setup.",
|
|
75
|
+
"项目需要完成 Silvermoon 整备。",
|
|
76
|
+
),
|
|
77
|
+
"repository-preparation-required": localize(
|
|
78
|
+
language,
|
|
79
|
+
"The local repository must be prepared before creating an idea.",
|
|
80
|
+
"创建 idea 前必须先整备本地 repository。",
|
|
81
|
+
),
|
|
82
|
+
"repository-sync-required": localize(
|
|
83
|
+
language,
|
|
84
|
+
"The repository must be synchronized before this command can continue.",
|
|
85
|
+
"继续此命令前必须先同步 repository。",
|
|
86
|
+
),
|
|
87
|
+
};
|
|
88
|
+
return summaries[observation.state] ?? localize(
|
|
89
|
+
language,
|
|
90
|
+
`Command ${command} is blocked.`,
|
|
91
|
+
`命令 ${command} 当前受阻。`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function validationResponse(intention, observation, language) {
|
|
96
|
+
const valid = observation.state === "project-ready";
|
|
97
|
+
return {
|
|
98
|
+
kind: "validation-result",
|
|
99
|
+
language,
|
|
100
|
+
summary: valid
|
|
101
|
+
? localize(
|
|
102
|
+
language,
|
|
103
|
+
"The requested Silvermoon snapshot is valid.",
|
|
104
|
+
"请求的 Silvermoon snapshot 验证通过。",
|
|
105
|
+
)
|
|
106
|
+
: blockedSummary(intention.command, observation, language),
|
|
107
|
+
validation: {
|
|
108
|
+
target: clone(intention.args.target),
|
|
109
|
+
valid,
|
|
110
|
+
...(observation.version === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { version: clone(observation.version) }),
|
|
113
|
+
},
|
|
114
|
+
problems: clone(observation.problems ?? []),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function dialogueResponse(intention, internalObservation, language) {
|
|
119
|
+
const observation = internalObservation.observation;
|
|
120
|
+
const context = internalObservation.responseContext ?? {};
|
|
121
|
+
const base = {
|
|
122
|
+
language,
|
|
123
|
+
details: observationDetails(observation),
|
|
124
|
+
};
|
|
125
|
+
const nextSteps = normalizeNextSteps(context.nextSteps);
|
|
126
|
+
|
|
127
|
+
if (BLOCKED_STATES.has(observation.state)) {
|
|
128
|
+
return {
|
|
129
|
+
...base,
|
|
130
|
+
kind: "blocked",
|
|
131
|
+
summary: blockedSummary(intention.command, observation, language),
|
|
132
|
+
problems: clone(observation.problems ?? []),
|
|
133
|
+
nextSteps,
|
|
134
|
+
...(observation.selectedIdea === undefined
|
|
135
|
+
? {}
|
|
136
|
+
: { idea: clone(observation.selectedIdea) }),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (intention.command === "create-idea" && observation.state === "idea-created") {
|
|
141
|
+
return {
|
|
142
|
+
...base,
|
|
143
|
+
kind: "idea-created",
|
|
144
|
+
summary: localize(
|
|
145
|
+
language,
|
|
146
|
+
`Created idea ${observation.createdIdea.id}.`,
|
|
147
|
+
`已创建 idea ${observation.createdIdea.id}。`,
|
|
148
|
+
),
|
|
149
|
+
createdIdea: clone(observation.createdIdea),
|
|
150
|
+
nextSteps,
|
|
151
|
+
...(observation.guidance === undefined
|
|
152
|
+
? {}
|
|
153
|
+
: { guidance: clone(observation.guidance) }),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (intention.command === "whats-next") {
|
|
158
|
+
if (observation.state === "navigation-ready") {
|
|
159
|
+
const choices = clone(observation.ideas?.activeIdeas ?? []);
|
|
160
|
+
return {
|
|
161
|
+
...base,
|
|
162
|
+
kind: "choice-required",
|
|
163
|
+
summary: choices.length === 0
|
|
164
|
+
? localize(
|
|
165
|
+
language,
|
|
166
|
+
"No active ideas are available.",
|
|
167
|
+
"当前没有 active idea。",
|
|
168
|
+
)
|
|
169
|
+
: localize(
|
|
170
|
+
language,
|
|
171
|
+
`${choices.length} active idea(s) are available.`,
|
|
172
|
+
`当前有 ${choices.length} 个 active idea。`,
|
|
173
|
+
),
|
|
174
|
+
choices,
|
|
175
|
+
nextSteps,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (observation.state === "idea-not-found") {
|
|
179
|
+
return {
|
|
180
|
+
...base,
|
|
181
|
+
kind: "choice-required",
|
|
182
|
+
summary: localize(
|
|
183
|
+
language,
|
|
184
|
+
`Idea ${intention.args.idea} was not found.`,
|
|
185
|
+
`未找到 idea ${intention.args.idea}。`,
|
|
186
|
+
),
|
|
187
|
+
choices: clone(observation.candidates ?? []),
|
|
188
|
+
problems: clone(observation.problems ?? []),
|
|
189
|
+
nextSteps,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (observation.state === "idea-selected") {
|
|
193
|
+
const idea = clone(observation.selectedIdea);
|
|
194
|
+
return {
|
|
195
|
+
...base,
|
|
196
|
+
kind: "next-steps",
|
|
197
|
+
summary: localize(
|
|
198
|
+
language,
|
|
199
|
+
`Continue idea ${idea.alias ?? idea.id}; its state is ${idea.state}.`,
|
|
200
|
+
`继续推进 idea ${idea.alias ?? idea.id};当前状态为 ${idea.state}。`,
|
|
201
|
+
),
|
|
202
|
+
idea,
|
|
203
|
+
nextSteps,
|
|
204
|
+
...(observation.guidance === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { guidance: clone(observation.guidance) }),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (intention.command === "list-ideas" && observation.state === "ideas-listed") {
|
|
212
|
+
return {
|
|
213
|
+
...base,
|
|
214
|
+
kind: "idea-list",
|
|
215
|
+
summary: localize(
|
|
216
|
+
language,
|
|
217
|
+
`Matched ${observation.summary.matched} idea(s) and returned ${observation.summary.returned}.`,
|
|
218
|
+
`匹配 ${observation.summary.matched} 个 idea,返回 ${observation.summary.returned} 个。`,
|
|
219
|
+
),
|
|
220
|
+
query: clone(intention.args),
|
|
221
|
+
inventory: clone(observation.summary),
|
|
222
|
+
items: clone(observation.ideas),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Cannot produce a response for ${intention.command}:${observation.state}`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function respond(intention, internalObservation) {
|
|
232
|
+
if (!intention || !internalObservation?.observation) {
|
|
233
|
+
throw new Error("Response requires an intention and a final observation");
|
|
234
|
+
}
|
|
235
|
+
const language = responseLanguage(intention, internalObservation.observation);
|
|
236
|
+
if (intention.command === "check") {
|
|
237
|
+
return validationResponse(
|
|
238
|
+
intention,
|
|
239
|
+
internalObservation.observation,
|
|
240
|
+
language,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
return dialogueResponse(intention, internalObservation, language);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function codeSpan(value) {
|
|
247
|
+
const text = String(value);
|
|
248
|
+
let longest = 0;
|
|
249
|
+
for (const [run] of text.matchAll(/`+/g)) {
|
|
250
|
+
longest = Math.max(longest, run.length);
|
|
251
|
+
}
|
|
252
|
+
const fence = "`".repeat(longest + 1);
|
|
253
|
+
return longest === 0 ? `${fence}${text}${fence}` : `${fence} ${text} ${fence}`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderVersion(version) {
|
|
257
|
+
if (!version) return null;
|
|
258
|
+
if (version.type === "commit" || version.type === "remote") {
|
|
259
|
+
return version.commit === null
|
|
260
|
+
? version.type
|
|
261
|
+
: `${version.type} ${version.commit}`;
|
|
262
|
+
}
|
|
263
|
+
return version.type;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderGuidance(guidance, language) {
|
|
267
|
+
const content = guidance.content
|
|
268
|
+
.split("\n")
|
|
269
|
+
.map((line) => line.length === 0 ? ">" : `> ${line}`)
|
|
270
|
+
.join("\n");
|
|
271
|
+
return [
|
|
272
|
+
`### ${localize(language, "Project phase guidance", "项目阶段 guidance")}`,
|
|
273
|
+
"",
|
|
274
|
+
`- ${localize(language, "source", "来源")}: ${localize(
|
|
275
|
+
language,
|
|
276
|
+
"repository-owned additive guidance",
|
|
277
|
+
"repository-owned 追加 guidance",
|
|
278
|
+
)}`,
|
|
279
|
+
`- ${localize(language, "phase", "阶段")}: ${codeSpan(guidance.phase)}`,
|
|
280
|
+
`- ${localize(language, "path", "路径")}: ${codeSpan(guidance.path)}`,
|
|
281
|
+
`- ${localize(language, "content revision", "内容 revision")}: ${codeSpan(guidance.contentRevision)}`,
|
|
282
|
+
"",
|
|
283
|
+
content,
|
|
284
|
+
].join("\n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function responseTitle(response) {
|
|
288
|
+
const titles = {
|
|
289
|
+
blocked: ["Blocked", "受阻"],
|
|
290
|
+
"choice-required": ["Choose what to continue", "选择要继续的工作"],
|
|
291
|
+
"idea-created": ["Idea created", "已创建 idea"],
|
|
292
|
+
"idea-list": ["Ideas", "Ideas"],
|
|
293
|
+
"next-steps": ["Next steps", "下一步"],
|
|
294
|
+
"validation-result": ["Check", "检查"],
|
|
295
|
+
};
|
|
296
|
+
const [english, chinese] = titles[response.kind] ?? ["Result", "结果"];
|
|
297
|
+
return localize(response.language, english, chinese);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function renderResponse(response) {
|
|
301
|
+
const language = response.language ?? DEFAULT_LANGUAGE;
|
|
302
|
+
const lines = [`## ${responseTitle(response)}`, "", response.summary];
|
|
303
|
+
|
|
304
|
+
if (response.inventory) {
|
|
305
|
+
const counts = Object.entries(response.inventory.counts)
|
|
306
|
+
.map(([state, count]) => `${state}=${count}`)
|
|
307
|
+
.join(", ");
|
|
308
|
+
lines.push(
|
|
309
|
+
"",
|
|
310
|
+
`- ${localize(language, "States", "状态")}: ${response.query.states
|
|
311
|
+
.map(codeSpan)
|
|
312
|
+
.join(", ")}`,
|
|
313
|
+
);
|
|
314
|
+
if (response.query.query !== null) {
|
|
315
|
+
lines.push(
|
|
316
|
+
`- ${localize(language, "Query", "查询")}: ${codeSpan(response.query.query)}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
if (response.query.createdSince !== null) {
|
|
320
|
+
lines.push(
|
|
321
|
+
`- ${localize(language, "Created since", "创建时间下界")}: ${codeSpan(response.query.createdSince)}`,
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (response.query.createdBefore !== null) {
|
|
325
|
+
lines.push(
|
|
326
|
+
`- ${localize(language, "Created before", "创建时间上界")}: ${codeSpan(response.query.createdBefore)}`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
lines.push(
|
|
330
|
+
`- ${localize(language, "Sort", "排序")}: ${codeSpan(response.query.sort)}`,
|
|
331
|
+
`- ${localize(language, "Limit", "上限")}: ${response.query.limit === null
|
|
332
|
+
? localize(language, "none", "无")
|
|
333
|
+
: codeSpan(response.query.limit)}`,
|
|
334
|
+
`- ${localize(language, "Matched", "匹配")}: ${response.inventory.matched}`,
|
|
335
|
+
`- ${localize(language, "Returned", "返回")}: ${response.inventory.returned}`,
|
|
336
|
+
`- ${localize(language, "Truncated", "已截断")}: ${response.inventory.truncated
|
|
337
|
+
? localize(language, "yes", "是")
|
|
338
|
+
: localize(language, "no", "否")}`,
|
|
339
|
+
`- ${localize(language, "Counts", "计数")}: ${counts}`,
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (response.validation) {
|
|
344
|
+
const target = response.validation.target.type === "commit"
|
|
345
|
+
? `${response.validation.target.type} ${response.validation.target.revision}`
|
|
346
|
+
: response.validation.target.type;
|
|
347
|
+
lines.push(
|
|
348
|
+
"",
|
|
349
|
+
`- ${localize(language, "Target", "目标")}: ${codeSpan(target)}`,
|
|
350
|
+
);
|
|
351
|
+
const version = renderVersion(response.validation.version);
|
|
352
|
+
if (version) {
|
|
353
|
+
lines.push(`- ${localize(language, "Version", "版本")}: ${codeSpan(version)}`);
|
|
354
|
+
}
|
|
355
|
+
lines.push(
|
|
356
|
+
`- ${localize(language, "Result", "结果")}: ${response.validation.valid
|
|
357
|
+
? localize(language, "valid", "通过")
|
|
358
|
+
: localize(language, "invalid or unavailable", "未通过或无法验证")}`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (response.idea) {
|
|
363
|
+
lines.push(
|
|
364
|
+
"",
|
|
365
|
+
`- ${localize(language, "Idea", "Idea")}: ${codeSpan(response.idea.id)}`,
|
|
366
|
+
`- ${localize(language, "State", "状态")}: ${codeSpan(response.idea.state)}`,
|
|
367
|
+
);
|
|
368
|
+
if (response.idea.alias !== undefined) {
|
|
369
|
+
lines.push(
|
|
370
|
+
`- ${localize(language, "Alias", "Alias")}: ${codeSpan(response.idea.alias)}`,
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (response.createdIdea) {
|
|
376
|
+
lines.push(
|
|
377
|
+
"",
|
|
378
|
+
`- ${localize(language, "ID", "ID")}: ${codeSpan(response.createdIdea.id)}`,
|
|
379
|
+
`- ${localize(language, "Path", "路径")}: ${codeSpan(response.createdIdea.path)}`,
|
|
380
|
+
`- ${localize(language, "State", "状态")}: ${codeSpan(response.createdIdea.state)}`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (response.choices?.length > 0) {
|
|
385
|
+
lines.push(
|
|
386
|
+
"",
|
|
387
|
+
`### ${localize(language, "Available ideas", "可用 ideas")}`,
|
|
388
|
+
"",
|
|
389
|
+
);
|
|
390
|
+
for (const idea of response.choices) {
|
|
391
|
+
const alias = idea.alias === undefined ? "" : ` (${idea.alias})`;
|
|
392
|
+
lines.push(`- ${codeSpan(idea.id)}${alias} ${idea.state}`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (response.items) {
|
|
397
|
+
lines.push("", `### ${localize(language, "Results", "结果")}`, "");
|
|
398
|
+
if (response.items.length === 0) {
|
|
399
|
+
lines.push(localize(language, "No ideas matched.", "没有匹配的 idea。"));
|
|
400
|
+
} else {
|
|
401
|
+
for (const idea of response.items) {
|
|
402
|
+
const details = [
|
|
403
|
+
`${localize(language, "State", "状态")}=${codeSpan(idea.state)}`,
|
|
404
|
+
`${localize(language, "Created", "创建时间")}=${codeSpan(idea.createdAt)}`,
|
|
405
|
+
];
|
|
406
|
+
if (idea.alias !== undefined) {
|
|
407
|
+
details.push(
|
|
408
|
+
`${localize(language, "Alias", "Alias")}=${codeSpan(idea.alias)}`,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
if (idea.title !== undefined) {
|
|
412
|
+
details.push(
|
|
413
|
+
`${localize(language, "Title", "标题")}=${codeSpan(idea.title)}`,
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
lines.push(`- ${codeSpan(idea.id)}: ${details.join("; ")}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (response.problems?.length > 0) {
|
|
422
|
+
lines.push(
|
|
423
|
+
"",
|
|
424
|
+
`### ${localize(language, "Issues to address", "需要处理的问题")}`,
|
|
425
|
+
"",
|
|
426
|
+
);
|
|
427
|
+
for (const problem of response.problems) {
|
|
428
|
+
lines.push(`- [${problem.type}] ${problem.summary}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (response.nextSteps?.length > 0) {
|
|
433
|
+
lines.push("", `### ${localize(language, "Next steps", "下一步")}`, "");
|
|
434
|
+
for (const step of response.nextSteps) {
|
|
435
|
+
lines.push(step.text);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (response.guidance !== undefined) {
|
|
440
|
+
lines.push("", renderGuidance(response.guidance, language));
|
|
441
|
+
}
|
|
442
|
+
return lines.join("\n");
|
|
443
|
+
}
|
package/src/trace.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { access, lstat, writeFile } from "node:fs/promises";
|
|
5
5
|
import { dirname, resolve } from "node:path";
|
|
6
6
|
|
|
7
|
-
const TRACE_SCHEMA_VERSION =
|
|
7
|
+
export const TRACE_SCHEMA_VERSION = 2;
|
|
8
8
|
const traceContext = new AsyncLocalStorage();
|
|
9
9
|
|
|
10
10
|
function durationMilliseconds(started) {
|
|
@@ -44,6 +44,7 @@ class Trace {
|
|
|
44
44
|
};
|
|
45
45
|
this.nextSpanId += 1;
|
|
46
46
|
this.emit({
|
|
47
|
+
channel: "telemetry",
|
|
47
48
|
event: "span-start",
|
|
48
49
|
spanId: span.id,
|
|
49
50
|
parentSpanId,
|
|
@@ -55,6 +56,7 @@ class Trace {
|
|
|
55
56
|
|
|
56
57
|
end(span, status, attributes = {}) {
|
|
57
58
|
this.emit({
|
|
59
|
+
channel: "telemetry",
|
|
58
60
|
event: "span-end",
|
|
59
61
|
spanId: span.id,
|
|
60
62
|
parentSpanId: span.parentSpanId,
|
|
@@ -86,7 +88,12 @@ async function requireAvailableTracePath(path) {
|
|
|
86
88
|
throw new Error(`Trace file already exists: ${path}`);
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
export async function traceAsync(
|
|
91
|
+
export async function traceAsync(
|
|
92
|
+
name,
|
|
93
|
+
attributes,
|
|
94
|
+
callback,
|
|
95
|
+
summarize = () => ({}),
|
|
96
|
+
) {
|
|
90
97
|
const context = traceContext.getStore();
|
|
91
98
|
if (!context) return callback();
|
|
92
99
|
|
|
@@ -96,7 +103,12 @@ export async function traceAsync(name, attributes, callback) {
|
|
|
96
103
|
async () => {
|
|
97
104
|
try {
|
|
98
105
|
const result = await callback();
|
|
99
|
-
|
|
106
|
+
const summary = summarize(result);
|
|
107
|
+
context.trace.end(
|
|
108
|
+
span,
|
|
109
|
+
summary.status ?? "ok",
|
|
110
|
+
summary.attributes ?? {},
|
|
111
|
+
);
|
|
100
112
|
return result;
|
|
101
113
|
} catch (caught) {
|
|
102
114
|
context.trace.end(span, "error", { errorName: errorName(caught) });
|
|
@@ -106,6 +118,80 @@ export async function traceAsync(name, attributes, callback) {
|
|
|
106
118
|
);
|
|
107
119
|
}
|
|
108
120
|
|
|
121
|
+
function hash(value) {
|
|
122
|
+
return createHash("sha256")
|
|
123
|
+
.update(JSON.stringify(value))
|
|
124
|
+
.digest("hex");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function projectDomainMessage(message) {
|
|
128
|
+
const base = {
|
|
129
|
+
channel: "domain",
|
|
130
|
+
event: message.type,
|
|
131
|
+
messageSequence: message.sequence,
|
|
132
|
+
};
|
|
133
|
+
if (message.type === "intention.accepted") {
|
|
134
|
+
return {
|
|
135
|
+
...base,
|
|
136
|
+
command: message.intention.command,
|
|
137
|
+
argumentNames: Object.keys(message.intention.args ?? {}).sort(),
|
|
138
|
+
intentionHash: hash(message.intention),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (message.type === "observation.fact") {
|
|
142
|
+
return {
|
|
143
|
+
...base,
|
|
144
|
+
factType: message.fact.type,
|
|
145
|
+
progress: message.fact.progress,
|
|
146
|
+
state: message.fact.observation.state,
|
|
147
|
+
problemCount: message.fact.observation.problems?.length ?? 0,
|
|
148
|
+
problemTypes: (message.fact.observation.problems ?? [])
|
|
149
|
+
.map(({ type }) => type),
|
|
150
|
+
...(message.fact.observation.selectedIdea?.id === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { selectedIdeaId: message.fact.observation.selectedIdea.id }),
|
|
153
|
+
...(message.fact.observation.createdIdea?.id === undefined
|
|
154
|
+
? {}
|
|
155
|
+
: { createdIdeaId: message.fact.observation.createdIdea.id }),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (message.type === "action.requested") {
|
|
159
|
+
return {
|
|
160
|
+
...base,
|
|
161
|
+
actionId: message.actionId,
|
|
162
|
+
actionType: message.action.type,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
if (message.type === "action.finished") {
|
|
166
|
+
return {
|
|
167
|
+
...base,
|
|
168
|
+
actionId: message.actionId,
|
|
169
|
+
actionType: message.actionType,
|
|
170
|
+
status: message.status,
|
|
171
|
+
...(message.problem?.type === undefined
|
|
172
|
+
? {}
|
|
173
|
+
: { problemType: message.problem.type }),
|
|
174
|
+
resultFields: Object.keys(message.result ?? {}).sort(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (message.type === "response.created") {
|
|
178
|
+
return {
|
|
179
|
+
...base,
|
|
180
|
+
kind: message.kind,
|
|
181
|
+
responseHash: message.metadata.hash,
|
|
182
|
+
nextStepCount: message.metadata.nextStepCount,
|
|
183
|
+
itemCount: message.metadata.itemCount,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
throw new Error(`Cannot trace unknown domain message type: ${message.type}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function emitDomainMessage(message) {
|
|
190
|
+
const context = traceContext.getStore();
|
|
191
|
+
if (!context) return;
|
|
192
|
+
context.trace.emit(projectDomainMessage(message));
|
|
193
|
+
}
|
|
194
|
+
|
|
109
195
|
export function traceSync(name, attributes, callback, summarize = () => ({})) {
|
|
110
196
|
const context = traceContext.getStore();
|
|
111
197
|
if (!context) return callback();
|