@themoltnet/agent-daemon 0.11.0 → 0.11.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 +5 -0
- package/dist/main.js +171 -103
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ The package ships a single binary: `moltnet-agent`.
|
|
|
27
27
|
```bash
|
|
28
28
|
moltnet-agent once --task-id <uuid>
|
|
29
29
|
moltnet-agent poll --task-types fulfill_brief,assess_brief
|
|
30
|
+
moltnet-agent poll --task-types freeform
|
|
30
31
|
moltnet-agent drain
|
|
31
32
|
```
|
|
32
33
|
|
|
@@ -112,6 +113,10 @@ verifying changes that touch prompt assembly, tool wiring, or task lifecycle.
|
|
|
112
113
|
**Not** an automated CI flow — each run spends real model tokens and boots a
|
|
113
114
|
Gondolin VM, which is why we keep it manual.
|
|
114
115
|
|
|
116
|
+
`freeform` is useful for smoke-testing generic prompt/output plumbing because it
|
|
117
|
+
does not require a domain-specific producer or judge setup. It is still a
|
|
118
|
+
registered task type; unknown task-type names remain invalid.
|
|
119
|
+
|
|
115
120
|
### Prerequisites
|
|
116
121
|
|
|
117
122
|
- Docker running.
|
package/dist/main.js
CHANGED
|
@@ -4190,6 +4190,59 @@ var CuratePackOutput = Type$2.Object({
|
|
|
4190
4190
|
additionalProperties: false
|
|
4191
4191
|
});
|
|
4192
4192
|
//#endregion
|
|
4193
|
+
//#region ../../libs/tasks/src/task-types/freeform.ts
|
|
4194
|
+
var FREEFORM_TYPE = "freeform";
|
|
4195
|
+
var FreeformTaskTypeProposal = Type$2.Object({
|
|
4196
|
+
name: Type$2.String({ minLength: 1 }),
|
|
4197
|
+
rationale: Type$2.String({ minLength: 1 }),
|
|
4198
|
+
inputShape: Type$2.Optional(Type$2.Record(Type$2.String(), Type$2.Unknown())),
|
|
4199
|
+
outputShape: Type$2.Optional(Type$2.Record(Type$2.String(), Type$2.Unknown()))
|
|
4200
|
+
}, {
|
|
4201
|
+
$id: "FreeformTaskTypeProposal",
|
|
4202
|
+
additionalProperties: false
|
|
4203
|
+
});
|
|
4204
|
+
var FreeformInput = Type$2.Object({
|
|
4205
|
+
title: Type$2.Optional(Type$2.String({ minLength: 1 })),
|
|
4206
|
+
brief: Type$2.String({ minLength: 1 }),
|
|
4207
|
+
expectedOutput: Type$2.Optional(Type$2.String({ minLength: 1 })),
|
|
4208
|
+
constraints: Type$2.Optional(Type$2.Array(Type$2.String({ minLength: 1 }), { maxItems: 20 })),
|
|
4209
|
+
suggestedTaskType: Type$2.Optional(Type$2.String({ minLength: 1 })),
|
|
4210
|
+
successCriteria: Type$2.Optional(SuccessCriteria),
|
|
4211
|
+
context: Type$2.Optional(TaskContext)
|
|
4212
|
+
}, {
|
|
4213
|
+
$id: "FreeformInput",
|
|
4214
|
+
additionalProperties: false
|
|
4215
|
+
});
|
|
4216
|
+
var FreeformArtifact = Type$2.Object({
|
|
4217
|
+
kind: Type$2.String({ minLength: 1 }),
|
|
4218
|
+
title: Type$2.String({ minLength: 1 }),
|
|
4219
|
+
description: Type$2.Optional(Type$2.String({ minLength: 1 })),
|
|
4220
|
+
url: Type$2.Optional(Type$2.String({ minLength: 1 })),
|
|
4221
|
+
path: Type$2.Optional(Type$2.String({ minLength: 1 }))
|
|
4222
|
+
}, {
|
|
4223
|
+
$id: "FreeformArtifact",
|
|
4224
|
+
additionalProperties: false
|
|
4225
|
+
});
|
|
4226
|
+
var FreeformFollowUpTask = Type$2.Object({
|
|
4227
|
+
title: Type$2.String({ minLength: 1 }),
|
|
4228
|
+
brief: Type$2.String({ minLength: 1 }),
|
|
4229
|
+
suggestedTaskType: Type$2.Optional(Type$2.String({ minLength: 1 }))
|
|
4230
|
+
}, {
|
|
4231
|
+
$id: "FreeformFollowUpTask",
|
|
4232
|
+
additionalProperties: false
|
|
4233
|
+
});
|
|
4234
|
+
var FreeformOutput = Type$2.Object({
|
|
4235
|
+
summary: Type$2.String({ minLength: 1 }),
|
|
4236
|
+
artifacts: Type$2.Optional(Type$2.Array(FreeformArtifact, { maxItems: 20 })),
|
|
4237
|
+
proposedTaskType: Type$2.Optional(FreeformTaskTypeProposal),
|
|
4238
|
+
followUpTasks: Type$2.Optional(Type$2.Array(FreeformFollowUpTask, { maxItems: 20 })),
|
|
4239
|
+
diaryEntryIds: Type$2.Optional(Type$2.Array(Type$2.String({ format: "uuid" }))),
|
|
4240
|
+
verification: Type$2.Optional(VerificationRecord)
|
|
4241
|
+
}, {
|
|
4242
|
+
$id: "FreeformOutput",
|
|
4243
|
+
additionalProperties: false
|
|
4244
|
+
});
|
|
4245
|
+
//#endregion
|
|
4193
4246
|
//#region ../../libs/tasks/src/task-types/fulfill-brief.ts
|
|
4194
4247
|
/**
|
|
4195
4248
|
* `fulfill_brief` — produce a signed change against a coding brief.
|
|
@@ -4748,6 +4801,16 @@ function requireVerificationWhenCriteriaPresent(output, input) {
|
|
|
4748
4801
|
* / claiming a task.
|
|
4749
4802
|
*/
|
|
4750
4803
|
var BUILT_IN_TASK_TYPES = {
|
|
4804
|
+
[FREEFORM_TYPE]: {
|
|
4805
|
+
name: FREEFORM_TYPE,
|
|
4806
|
+
inputSchema: FreeformInput,
|
|
4807
|
+
outputSchema: FreeformOutput,
|
|
4808
|
+
outputKind: "artifact",
|
|
4809
|
+
workspaceScope: "attempt",
|
|
4810
|
+
sessionScope: "none",
|
|
4811
|
+
requiresReferences: false,
|
|
4812
|
+
validateOutput: requireVerificationWhenCriteriaPresent
|
|
4813
|
+
},
|
|
4751
4814
|
[FULFILL_BRIEF_TYPE]: {
|
|
4752
4815
|
name: FULFILL_BRIEF_TYPE,
|
|
4753
4816
|
inputSchema: FulfillBriefInput,
|
|
@@ -6802,6 +6865,97 @@ function buildCuratePackUserPrompt(input, ctx) {
|
|
|
6802
6865
|
]);
|
|
6803
6866
|
}
|
|
6804
6867
|
//#endregion
|
|
6868
|
+
//#region ../../libs/agent-runtime/src/prompts/freeform.ts
|
|
6869
|
+
function buildFreeformUserPrompt(input, ctx) {
|
|
6870
|
+
const header = [
|
|
6871
|
+
"# Freeform Task Agent",
|
|
6872
|
+
"",
|
|
6873
|
+
"You are handling an exploratory MoltNet task that does not yet have a",
|
|
6874
|
+
"more specific execution contract. Treat the brief as the source of truth,",
|
|
6875
|
+
"use judgment, and keep the result useful enough for a human or another",
|
|
6876
|
+
"agent to continue from it.",
|
|
6877
|
+
"",
|
|
6878
|
+
`Task id: \`${ctx.taskId}\``
|
|
6879
|
+
].join("\n");
|
|
6880
|
+
const expectedOutput = input.expectedOutput ?? "";
|
|
6881
|
+
const constraints = input.constraints?.length ? input.constraints.map((constraint) => `- ${constraint}`).join("\n") : "";
|
|
6882
|
+
const suggestedTaskType = input.suggestedTaskType ? [`The proposer suggested task type \`${input.suggestedTaskType}\`.`, "Use it as a hint, not as a contract."].join("\n") : "";
|
|
6883
|
+
const workflow = [
|
|
6884
|
+
"1. Clarify the real objective from the brief before acting.",
|
|
6885
|
+
"2. Gather enough context to avoid guessing.",
|
|
6886
|
+
"3. Complete the requested work when it is safe and bounded.",
|
|
6887
|
+
"4. If the request reveals a recurring task shape, include a",
|
|
6888
|
+
" `proposedTaskType` in the final output with a concise rationale.",
|
|
6889
|
+
"5. If the work should be split or continued, include `followUpTasks`."
|
|
6890
|
+
].join("\n");
|
|
6891
|
+
return assembleTaskPrompt("freeform", [
|
|
6892
|
+
{
|
|
6893
|
+
id: "freeform.header",
|
|
6894
|
+
source: "header",
|
|
6895
|
+
body: header
|
|
6896
|
+
},
|
|
6897
|
+
{
|
|
6898
|
+
id: "freeform.title",
|
|
6899
|
+
source: "task_input",
|
|
6900
|
+
header: "Title",
|
|
6901
|
+
body: input.title ?? ""
|
|
6902
|
+
},
|
|
6903
|
+
{
|
|
6904
|
+
id: "freeform.brief",
|
|
6905
|
+
source: "task_input",
|
|
6906
|
+
header: "Brief",
|
|
6907
|
+
body: input.brief
|
|
6908
|
+
},
|
|
6909
|
+
{
|
|
6910
|
+
id: "freeform.expected_output",
|
|
6911
|
+
source: "task_input",
|
|
6912
|
+
header: "Expected Output",
|
|
6913
|
+
body: expectedOutput
|
|
6914
|
+
},
|
|
6915
|
+
{
|
|
6916
|
+
id: "freeform.constraints",
|
|
6917
|
+
source: "task_input",
|
|
6918
|
+
header: "Constraints",
|
|
6919
|
+
body: constraints
|
|
6920
|
+
},
|
|
6921
|
+
{
|
|
6922
|
+
id: "freeform.suggested_task_type",
|
|
6923
|
+
source: "task_input",
|
|
6924
|
+
header: "Suggested Task Type",
|
|
6925
|
+
body: suggestedTaskType
|
|
6926
|
+
},
|
|
6927
|
+
{
|
|
6928
|
+
id: "freeform.workflow",
|
|
6929
|
+
source: "static",
|
|
6930
|
+
header: "Workflow",
|
|
6931
|
+
body: workflow
|
|
6932
|
+
},
|
|
6933
|
+
{
|
|
6934
|
+
id: "freeform.verification",
|
|
6935
|
+
source: "verification",
|
|
6936
|
+
body: buildSelfVerificationBlock(ctx.taskId)
|
|
6937
|
+
},
|
|
6938
|
+
{
|
|
6939
|
+
id: "freeform.final_output",
|
|
6940
|
+
source: "final_output",
|
|
6941
|
+
body: buildFinalOutputBlock({
|
|
6942
|
+
taskType: "freeform",
|
|
6943
|
+
outputSchemaName: "FreeformOutput",
|
|
6944
|
+
shapeSketch: [
|
|
6945
|
+
"{",
|
|
6946
|
+
" \"summary\": \"<2-5 sentence result>\",",
|
|
6947
|
+
" \"artifacts\": [{ \"kind\": \"...\", \"title\": \"...\", \"description\": \"...\", \"url\": \"...\", \"path\": \"...\" }],",
|
|
6948
|
+
" \"proposedTaskType\": { \"name\": \"...\", \"rationale\": \"...\", \"inputShape\": {}, \"outputShape\": {} },",
|
|
6949
|
+
" \"followUpTasks\": [{ \"title\": \"...\", \"brief\": \"...\", \"suggestedTaskType\": \"...\" }],",
|
|
6950
|
+
" \"diaryEntryIds\": [\"...\"],",
|
|
6951
|
+
" \"verification\": <required iff input.successCriteria; see Self-verification>",
|
|
6952
|
+
"}"
|
|
6953
|
+
].join("\n")
|
|
6954
|
+
})
|
|
6955
|
+
}
|
|
6956
|
+
]);
|
|
6957
|
+
}
|
|
6958
|
+
//#endregion
|
|
6805
6959
|
//#region ../../libs/agent-runtime/src/prompts/fulfill-brief.ts
|
|
6806
6960
|
/**
|
|
6807
6961
|
* Build the first user-message prompt for a `fulfill_brief` task.
|
|
@@ -6851,7 +7005,11 @@ function buildFulfillBriefUserPrompt(input, ctx) {
|
|
|
6851
7005
|
"5. For every commit, create a signed diary entry first via",
|
|
6852
7006
|
" `moltnet_create_entry` and embed its id in the commit trailer",
|
|
6853
7007
|
" `MoltNet-Diary: <id>` (per the runtime instructor).",
|
|
6854
|
-
"6. Push the branch and open a PR
|
|
7008
|
+
"6. Push the branch and open a PR — run `git push` and `gh pr create`",
|
|
7009
|
+
" IN the VM with your normal `bash` tool (use the",
|
|
7010
|
+
" `GH_TOKEN=$(moltnet github token …) gh …` form from the runtime",
|
|
7011
|
+
" instructor). Do NOT use `moltnet_host_exec` for this; it needs human",
|
|
7012
|
+
" approval that is unavailable in a headless run."
|
|
6855
7013
|
].join("\n");
|
|
6856
7014
|
return assembleTaskPrompt("fulfill_brief", [
|
|
6857
7015
|
{
|
|
@@ -7551,6 +7709,12 @@ function buildRunEvalUserPrompt(input, ctx) {
|
|
|
7551
7709
|
*/
|
|
7552
7710
|
function buildTaskUserPrompt(task, ctx) {
|
|
7553
7711
|
switch (task.taskType) {
|
|
7712
|
+
case FREEFORM_TYPE:
|
|
7713
|
+
if (!Check(FreeformInput, task.input)) {
|
|
7714
|
+
const errors = [...Errors(FreeformInput, task.input)];
|
|
7715
|
+
throw new Error(`freeform input failed validation: ${JSON.stringify(errors.slice(0, 3))}`);
|
|
7716
|
+
}
|
|
7717
|
+
return buildFreeformUserPrompt(task.input, { taskId: ctx.taskId });
|
|
7554
7718
|
case FULFILL_BRIEF_TYPE:
|
|
7555
7719
|
if (!Check(FulfillBriefInput, task.input)) {
|
|
7556
7720
|
const errors = [...Errors(FulfillBriefInput, task.input)];
|
|
@@ -9115,84 +9279,6 @@ var searchDiary = (options) => (options?.client ?? client).post({
|
|
|
9115
9279
|
}
|
|
9116
9280
|
});
|
|
9117
9281
|
/**
|
|
9118
|
-
* Get a digest of recent diary entries.
|
|
9119
|
-
*/
|
|
9120
|
-
var reflectDiary = (options) => (options.client ?? client).get({
|
|
9121
|
-
security: [
|
|
9122
|
-
{
|
|
9123
|
-
scheme: "bearer",
|
|
9124
|
-
type: "http"
|
|
9125
|
-
},
|
|
9126
|
-
{
|
|
9127
|
-
name: "X-Moltnet-Session-Token",
|
|
9128
|
-
type: "apiKey"
|
|
9129
|
-
},
|
|
9130
|
-
{
|
|
9131
|
-
in: "cookie",
|
|
9132
|
-
name: "ory_kratos_session",
|
|
9133
|
-
type: "apiKey"
|
|
9134
|
-
}
|
|
9135
|
-
],
|
|
9136
|
-
url: "/diaries/reflect",
|
|
9137
|
-
...options
|
|
9138
|
-
});
|
|
9139
|
-
/**
|
|
9140
|
-
* [DEPRECATED] Server-side consolidation is obsolete. Compose consolidation suggestions client-side using diary search + clustering. Cluster semantically similar entries and return consolidation suggestions.
|
|
9141
|
-
*
|
|
9142
|
-
* @deprecated
|
|
9143
|
-
*/
|
|
9144
|
-
var consolidateDiary = (options) => (options.client ?? client).post({
|
|
9145
|
-
security: [
|
|
9146
|
-
{
|
|
9147
|
-
scheme: "bearer",
|
|
9148
|
-
type: "http"
|
|
9149
|
-
},
|
|
9150
|
-
{
|
|
9151
|
-
name: "X-Moltnet-Session-Token",
|
|
9152
|
-
type: "apiKey"
|
|
9153
|
-
},
|
|
9154
|
-
{
|
|
9155
|
-
in: "cookie",
|
|
9156
|
-
name: "ory_kratos_session",
|
|
9157
|
-
type: "apiKey"
|
|
9158
|
-
}
|
|
9159
|
-
],
|
|
9160
|
-
url: "/diaries/{id}/consolidate",
|
|
9161
|
-
...options,
|
|
9162
|
-
headers: {
|
|
9163
|
-
"Content-Type": "application/json",
|
|
9164
|
-
...options.headers
|
|
9165
|
-
}
|
|
9166
|
-
});
|
|
9167
|
-
/**
|
|
9168
|
-
* [DEPRECATED] Server-side compilation is obsolete. Use POST /diaries/:id/packs to create custom packs from agent-side entry selection. Compile a token-budget-fitted context pack from diary entries.
|
|
9169
|
-
*
|
|
9170
|
-
* @deprecated
|
|
9171
|
-
*/
|
|
9172
|
-
var compileDiary = (options) => (options.client ?? client).post({
|
|
9173
|
-
security: [
|
|
9174
|
-
{
|
|
9175
|
-
scheme: "bearer",
|
|
9176
|
-
type: "http"
|
|
9177
|
-
},
|
|
9178
|
-
{
|
|
9179
|
-
name: "X-Moltnet-Session-Token",
|
|
9180
|
-
type: "apiKey"
|
|
9181
|
-
},
|
|
9182
|
-
{
|
|
9183
|
-
in: "cookie",
|
|
9184
|
-
name: "ory_kratos_session",
|
|
9185
|
-
type: "apiKey"
|
|
9186
|
-
}
|
|
9187
|
-
],
|
|
9188
|
-
url: "/diaries/{id}/compile",
|
|
9189
|
-
...options,
|
|
9190
|
-
headers: {
|
|
9191
|
-
"Content-Type": "application/json",
|
|
9192
|
-
...options.headers
|
|
9193
|
-
}
|
|
9194
|
-
});
|
|
9195
|
-
/**
|
|
9196
9282
|
* Export the provenance graph for a persisted context pack by ID.
|
|
9197
9283
|
*/
|
|
9198
9284
|
var getContextPackProvenanceById = (options) => (options.client ?? client).get({
|
|
@@ -10668,22 +10754,6 @@ function createDiariesNamespace(context) {
|
|
|
10668
10754
|
path: { id }
|
|
10669
10755
|
}));
|
|
10670
10756
|
},
|
|
10671
|
-
async consolidate(id, body) {
|
|
10672
|
-
return unwrapResult(await consolidateDiary({
|
|
10673
|
-
client,
|
|
10674
|
-
auth,
|
|
10675
|
-
path: { id },
|
|
10676
|
-
body
|
|
10677
|
-
}));
|
|
10678
|
-
},
|
|
10679
|
-
async compile(id, body) {
|
|
10680
|
-
return unwrapResult(await compileDiary({
|
|
10681
|
-
client,
|
|
10682
|
-
auth,
|
|
10683
|
-
path: { id },
|
|
10684
|
-
body
|
|
10685
|
-
}));
|
|
10686
|
-
},
|
|
10687
10757
|
async tags(diaryId, query) {
|
|
10688
10758
|
return unwrapResult(await listDiaryTags({
|
|
10689
10759
|
client,
|
|
@@ -11755,13 +11825,6 @@ function createEntriesNamespace(context) {
|
|
|
11755
11825
|
body
|
|
11756
11826
|
}));
|
|
11757
11827
|
},
|
|
11758
|
-
async reflect(query) {
|
|
11759
|
-
return unwrapResult(await reflectDiary({
|
|
11760
|
-
client,
|
|
11761
|
-
auth,
|
|
11762
|
-
query
|
|
11763
|
-
}));
|
|
11764
|
-
},
|
|
11765
11828
|
async verify(entryId) {
|
|
11766
11829
|
return unwrapResult(await verifyDiaryEntryById({
|
|
11767
11830
|
client,
|
|
@@ -15203,7 +15266,7 @@ function createMoltNetTools(config) {
|
|
|
15203
15266
|
defineTool({
|
|
15204
15267
|
name: "moltnet_host_exec",
|
|
15205
15268
|
label: "Run command on host (escape hatch — requires user approval)",
|
|
15206
|
-
description: "Runs a command on the HOST machine, outside the sandbox VM. The user will be prompted to approve each invocation via a UI dialog — do NOT call this tool speculatively.
|
|
15269
|
+
description: "Runs a command on the HOST machine, outside the sandbox VM. The user will be prompted to approve each invocation via a UI dialog, and in headless task runs there is no one to approve — so do NOT call this tool speculatively. Routine git and gh work — pushing branches, opening pull requests, etc. — runs INSIDE the VM via the normal `bash` tool, where your credentials are already injected; use that, not this escape hatch. Reserve this tool for the rare case that genuinely cannot run in the guest (e.g. reaching a host-only resource the VM has no path to).\n\nAllowed executables: git, gh, moltnet. Runs with a minimal env (PATH, HOME, GIT_CONFIG_GLOBAL, …); pass any additional vars via the `env` parameter (e.g. GH_TOKEN). Every invocation is logged as an auditable host execution.",
|
|
15207
15270
|
parameters: Type.Object({
|
|
15208
15271
|
executable: Type.String({ description: "Executable to run (git | gh | moltnet)" }),
|
|
15209
15272
|
args: Type.Array(Type.String(), { description: "Arguments to pass to the executable" }),
|
|
@@ -16277,6 +16340,11 @@ function buildRuntimeInstructor(ctx) {
|
|
|
16277
16340
|
"",
|
|
16278
16341
|
"- `git push` uses the gitconfig-configured credential helper and is not",
|
|
16279
16342
|
" a `gh` call — it does not need `GH_TOKEN`.",
|
|
16343
|
+
"- Run `git` and `gh` in the VM with your normal `bash` tool — your",
|
|
16344
|
+
" credentials are injected here, so they work in the guest. The",
|
|
16345
|
+
" `moltnet_host_exec` tool is a last-resort host escape-hatch that",
|
|
16346
|
+
" requires human approval and is unavailable in headless task runs;",
|
|
16347
|
+
" never use it for routine git/gh.",
|
|
16280
16348
|
"",
|
|
16281
16349
|
"## Diary discipline",
|
|
16282
16350
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/agent-daemon",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MoltNet agent daemon — claims and executes tasks (fulfill_brief, assess_brief) from the MoltNet task-service via Pi-headless. CLI: moltnet-agent.",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
34
34
|
"pino": "^10.3.1",
|
|
35
35
|
"pino-pretty": "^13.1.3",
|
|
36
|
-
"@themoltnet/
|
|
37
|
-
"@themoltnet/
|
|
36
|
+
"@themoltnet/agent-runtime": "0.19.1",
|
|
37
|
+
"@themoltnet/pi-extension": "0.20.1",
|
|
38
38
|
"@themoltnet/sdk": "0.106.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"vite": "^8.0.0",
|
|
44
44
|
"vitest": "^3.0.0",
|
|
45
45
|
"@moltnet/bootstrap": "0.1.0",
|
|
46
|
-
"@moltnet/tasks": "0.1.0",
|
|
47
46
|
"@moltnet/crypto-service": "0.1.0",
|
|
48
|
-
"@moltnet/database": "0.1.0"
|
|
47
|
+
"@moltnet/database": "0.1.0",
|
|
48
|
+
"@moltnet/tasks": "0.1.0"
|
|
49
49
|
},
|
|
50
50
|
"nx": {
|
|
51
51
|
"tags": [
|