@moxxy/core 0.26.0 → 0.28.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/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/plugins/host-options.d.ts +2 -0
- package/dist/plugins/host-options.d.ts.map +1 -1
- package/dist/plugins/host.d.ts +8 -0
- package/dist/plugins/host.d.ts.map +1 -1
- package/dist/plugins/host.js +14 -0
- package/dist/plugins/host.js.map +1 -1
- package/dist/plugins/registry-kinds.d.ts +1 -0
- package/dist/plugins/registry-kinds.d.ts.map +1 -1
- package/dist/plugins/registry-kinds.js +11 -0
- package/dist/plugins/registry-kinds.js.map +1 -1
- package/dist/registries/modes.d.ts +8 -0
- package/dist/registries/modes.d.ts.map +1 -1
- package/dist/registries/modes.js +11 -0
- package/dist/registries/modes.js.map +1 -1
- package/dist/registries/reflectors.d.ts +15 -0
- package/dist/registries/reflectors.d.ts.map +1 -0
- package/dist/registries/reflectors.js +16 -0
- package/dist/registries/reflectors.js.map +1 -0
- package/dist/run-turn.d.ts.map +1 -1
- package/dist/run-turn.js +5 -0
- package/dist/run-turn.js.map +1 -1
- package/dist/session-runtime.d.ts +4 -0
- package/dist/session-runtime.d.ts.map +1 -1
- package/dist/session.d.ts +9 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +17 -0
- package/dist/session.js.map +1 -1
- package/dist/sessions/event-shape.d.ts +68 -0
- package/dist/sessions/event-shape.d.ts.map +1 -0
- package/dist/sessions/event-shape.js +171 -0
- package/dist/sessions/event-shape.js.map +1 -0
- package/dist/sessions/persistence.d.ts +7 -5
- package/dist/sessions/persistence.d.ts.map +1 -1
- package/dist/sessions/persistence.js +39 -13
- package/dist/sessions/persistence.js.map +1 -1
- package/dist/skill-usage.d.ts +62 -0
- package/dist/skill-usage.d.ts.map +1 -0
- package/dist/skill-usage.js +106 -0
- package/dist/skill-usage.js.map +1 -0
- package/dist/skills/synthesize.d.ts.map +1 -1
- package/dist/skills/synthesize.js +42 -0
- package/dist/skills/synthesize.js.map +1 -1
- package/dist/subagents/run-child.d.ts.map +1 -1
- package/dist/subagents/run-child.js +4 -0
- package/dist/subagents/run-child.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +11 -0
- package/src/plugins/host-options.ts +2 -0
- package/src/plugins/host.ts +14 -0
- package/src/plugins/registry-kinds.test.ts +4 -0
- package/src/plugins/registry-kinds.ts +13 -0
- package/src/registries/modes.test.ts +27 -0
- package/src/registries/modes.ts +12 -0
- package/src/registries/reflectors.test.ts +50 -0
- package/src/registries/reflectors.ts +17 -0
- package/src/run-turn.ts +5 -0
- package/src/session-runtime.ts +4 -0
- package/src/session.ts +18 -0
- package/src/sessions/event-shape.test.ts +207 -0
- package/src/sessions/event-shape.ts +196 -0
- package/src/sessions/persistence.test.ts +132 -0
- package/src/sessions/persistence.ts +39 -13
- package/src/skill-usage.test.ts +102 -0
- package/src/skill-usage.ts +165 -0
- package/src/skills/synthesize.ts +42 -0
- package/src/subagents/run-child.ts +4 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import { createMutex } from '@moxxy/sdk';
|
|
3
|
+
import { moxxyPath, writeFileAtomic } from '@moxxy/sdk/server';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cross-session skill usage, persisted at `~/.moxxy/skills/.meta/usage.json`
|
|
8
|
+
* (next to the agent-created audit log). A forward-going aggregate keyed by
|
|
9
|
+
* skill NAME: each session folds its own `skill_invoked` / `skill_created`
|
|
10
|
+
* events and merges the delta in on shutdown (see `@moxxy/plugin-usage-stats`).
|
|
11
|
+
* Purely additive `invocations` counter plus first-`createdAt` / latest-
|
|
12
|
+
* `lastInvokedAt` timestamps — a session's contribution is added once and the
|
|
13
|
+
* file stays a monotone sum.
|
|
14
|
+
*
|
|
15
|
+
* KNOWN LIMITATION (as of this file's introduction): the only site that emits
|
|
16
|
+
* `skill_invoked` today is the `load_skill` tool (reason `'load_skill_tool'` —
|
|
17
|
+
* see `packages/core/src/skills/synthesize.ts`). The event type also carries
|
|
18
|
+
* `'trigger_match' | 'classifier' | 'manual'` reasons, but nothing emits those
|
|
19
|
+
* yet. So `invocations` counts ONLY explicit `load_skill` calls for now. When
|
|
20
|
+
* trigger-match / classifier emission lands later, this same file simply starts
|
|
21
|
+
* counting more — no format change required.
|
|
22
|
+
*
|
|
23
|
+
* Like `usage-stats.ts`, this is best-effort: a missing or malformed file reads
|
|
24
|
+
* as empty, and a write failure never blocks shutdown.
|
|
25
|
+
*/
|
|
26
|
+
export interface SkillUsage {
|
|
27
|
+
/** Total `load_skill`-driven invocations recorded across all sessions. */
|
|
28
|
+
readonly invocations: number;
|
|
29
|
+
/** ISO timestamp of the most recent recorded invocation, if any. */
|
|
30
|
+
readonly lastInvokedAt?: string;
|
|
31
|
+
/** ISO timestamp of when this skill was first agent-synthesized, if seen. */
|
|
32
|
+
readonly createdAt?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SkillUsageFile {
|
|
36
|
+
readonly version: 1;
|
|
37
|
+
/** ISO timestamp of the last merge. */
|
|
38
|
+
readonly updatedAt: string;
|
|
39
|
+
/** Per-skill-name lifetime usage. */
|
|
40
|
+
readonly skills: Record<string, SkillUsage>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One session's contribution for a single skill: how many invocations it saw,
|
|
45
|
+
* the latest invocation timestamp within the run, and — if the skill was
|
|
46
|
+
* created during the run — its creation timestamp.
|
|
47
|
+
*/
|
|
48
|
+
export interface SkillUsageDelta {
|
|
49
|
+
readonly invocations: number;
|
|
50
|
+
readonly lastInvokedAt?: string;
|
|
51
|
+
readonly createdAt?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function skillsUsagePath(): string {
|
|
55
|
+
// Route through `moxxyPath` so a `$MOXXY_HOME` override relocates the skill
|
|
56
|
+
// usage aggregate alongside the rest of the data dir. Identical to
|
|
57
|
+
// `~/.moxxy/skills/.meta/usage.json` when MOXXY_HOME is unset.
|
|
58
|
+
return moxxyPath('skills/.meta/usage.json');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function emptyUsage(): SkillUsageFile {
|
|
62
|
+
return { version: 1, updatedAt: new Date().toISOString(), skills: {} };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Validates the on-disk shape so a hand-edited or partially-written file with a
|
|
66
|
+
// non-numeric counter (e.g. `invocations: "3"`) can't flow into the additive
|
|
67
|
+
// merge and corrupt the persisted aggregate via string concatenation. A failed
|
|
68
|
+
// parse falls through to `emptyUsage()`, exactly like malformed JSON.
|
|
69
|
+
const skillUsageSchema = z.object({
|
|
70
|
+
invocations: z.number(),
|
|
71
|
+
lastInvokedAt: z.string().optional(),
|
|
72
|
+
createdAt: z.string().optional(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const skillUsageFileSchema = z.object({
|
|
76
|
+
version: z.literal(1),
|
|
77
|
+
updatedAt: z.string(),
|
|
78
|
+
skills: z.record(z.string(), skillUsageSchema),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Read the skill usage aggregate. Returns an empty file when missing or
|
|
83
|
+
* unparseable — skill usage is an optional, non-load-blocking layer.
|
|
84
|
+
*/
|
|
85
|
+
export async function loadSkillUsage(
|
|
86
|
+
filePath: string = skillsUsagePath(),
|
|
87
|
+
): Promise<SkillUsageFile> {
|
|
88
|
+
try {
|
|
89
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
90
|
+
const result = skillUsageFileSchema.safeParse(JSON.parse(raw));
|
|
91
|
+
if (result.success) return result.data;
|
|
92
|
+
// shape-invalid (e.g. a non-numeric counter) — start fresh rather than let
|
|
93
|
+
// a corrupt entry poison the aggregate via string-concat addition.
|
|
94
|
+
} catch {
|
|
95
|
+
// missing or malformed JSON — start fresh
|
|
96
|
+
}
|
|
97
|
+
return emptyUsage();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function writeAtomic(file: SkillUsageFile, filePath: string): Promise<void> {
|
|
101
|
+
await writeFileAtomic(filePath, JSON.stringify(file, null, 2) + '\n');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Serializes the read-modify-write in `mergeSkillUsage` so two concurrent merges
|
|
105
|
+
// (e.g. overlapping shutdowns) can't both read the same snapshot and have the
|
|
106
|
+
// second write clobber the first's delta.
|
|
107
|
+
const mergeMutex = createMutex();
|
|
108
|
+
|
|
109
|
+
/** Later of two ISO timestamps (lexicographic sort is correct for same-format
|
|
110
|
+
* UTC `toISOString()` output). Either may be absent. */
|
|
111
|
+
function laterIso(a: string | undefined, b: string | undefined): string | undefined {
|
|
112
|
+
if (!a) return b;
|
|
113
|
+
if (!b) return a;
|
|
114
|
+
return a >= b ? a : b;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Merge a session's per-skill delta into the persisted aggregate and write it
|
|
119
|
+
* back. Reads the current file, adds each `invocations` field-wise, keeps the
|
|
120
|
+
* earliest `createdAt` and the latest `lastInvokedAt`, and writes atomically.
|
|
121
|
+
* Returns the updated file.
|
|
122
|
+
*
|
|
123
|
+
* Best-effort: a write failure logs to stderr but does not throw — losing one
|
|
124
|
+
* session's stats must never block shutdown.
|
|
125
|
+
*/
|
|
126
|
+
export async function mergeSkillUsage(
|
|
127
|
+
delta: Record<string, SkillUsageDelta>,
|
|
128
|
+
filePath: string = skillsUsagePath(),
|
|
129
|
+
): Promise<SkillUsageFile> {
|
|
130
|
+
// An empty delta writes nothing, so there's no read-modify-write to serialize:
|
|
131
|
+
// skip the mutex and read the current file directly. A session that exercised
|
|
132
|
+
// no skills produces an empty delta on every shutdown, so this avoids needless
|
|
133
|
+
// I/O + contention on the common no-op path.
|
|
134
|
+
const keys = Object.keys(delta);
|
|
135
|
+
if (keys.length === 0) return loadSkillUsage(filePath);
|
|
136
|
+
|
|
137
|
+
return mergeMutex.run(async () => {
|
|
138
|
+
const current = await loadSkillUsage(filePath);
|
|
139
|
+
|
|
140
|
+
const now = new Date().toISOString();
|
|
141
|
+
const skills: Record<string, SkillUsage> = { ...current.skills };
|
|
142
|
+
for (const key of keys) {
|
|
143
|
+
const d = delta[key]!;
|
|
144
|
+
const existing = skills[key];
|
|
145
|
+
const invocations = (existing?.invocations ?? 0) + d.invocations;
|
|
146
|
+
const lastInvokedAt = laterIso(existing?.lastInvokedAt, d.lastInvokedAt);
|
|
147
|
+
const createdAt = existing?.createdAt ?? d.createdAt;
|
|
148
|
+
skills[key] = {
|
|
149
|
+
invocations,
|
|
150
|
+
...(lastInvokedAt ? { lastInvokedAt } : {}),
|
|
151
|
+
...(createdAt ? { createdAt } : {}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const next: SkillUsageFile = { version: 1, updatedAt: now, skills };
|
|
155
|
+
try {
|
|
156
|
+
await writeAtomic(next, filePath);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
`moxxy: failed to persist skill usage to ${filePath}: ` +
|
|
160
|
+
`${err instanceof Error ? err.message : String(err)}\n`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return next;
|
|
164
|
+
});
|
|
165
|
+
}
|
package/src/skills/synthesize.ts
CHANGED
|
@@ -213,6 +213,14 @@ export function buildSynthesizeSkillPlugin(
|
|
|
213
213
|
session: Session,
|
|
214
214
|
opts: SynthesizeOptions = {},
|
|
215
215
|
): Plugin {
|
|
216
|
+
// Capability fs globs for the tools below, resolved from the same option
|
|
217
|
+
// fallbacks the handlers use. The audit JSONL defaults under the DEFAULT
|
|
218
|
+
// user skills dir even when `userDir` is overridden (see synthesizeSkill).
|
|
219
|
+
const userSkillsGlob = `${opts.userDir ?? defaultUserSkillsDir()}/**`;
|
|
220
|
+
const projectSkillsGlob = `${opts.projectDir ?? defaultProjectSkillsDir(session.cwd)}/**`;
|
|
221
|
+
const auditGlob = opts.auditPath
|
|
222
|
+
? `${path.dirname(opts.auditPath)}/**`
|
|
223
|
+
: `${defaultUserSkillsDir()}/**`;
|
|
216
224
|
return definePlugin({
|
|
217
225
|
name: '@moxxy/synthesize-skill',
|
|
218
226
|
version: '0.0.0',
|
|
@@ -239,6 +247,19 @@ export function buildSynthesizeSkillPlugin(
|
|
|
239
247
|
),
|
|
240
248
|
}),
|
|
241
249
|
permission: { action: 'prompt' },
|
|
250
|
+
// Drafts the skill body via the active LLMProvider (host is
|
|
251
|
+
// provider-config dependent, hence net: any), then writes the skill
|
|
252
|
+
// file + the audit JSONL.
|
|
253
|
+
isolation: {
|
|
254
|
+
capabilities: {
|
|
255
|
+
fs: {
|
|
256
|
+
read: [auditGlob],
|
|
257
|
+
write: [userSkillsGlob, projectSkillsGlob, auditGlob],
|
|
258
|
+
},
|
|
259
|
+
net: { mode: 'any' },
|
|
260
|
+
timeMs: 120_000,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
242
263
|
handler: async ({ intent, scope }, ctx) => {
|
|
243
264
|
const result = await synthesizeSkill(session, intent, scope, opts, ctx.turnId);
|
|
244
265
|
return {
|
|
@@ -262,6 +283,8 @@ export function buildSynthesizeSkillPlugin(
|
|
|
262
283
|
.min(1)
|
|
263
284
|
.describe('The exact skill name from the "Available skills" list in the system prompt.'),
|
|
264
285
|
}),
|
|
286
|
+
// Registry lookup — the skill body is already in memory.
|
|
287
|
+
isolation: { capabilities: { net: { mode: 'none' }, timeMs: 10_000 } },
|
|
265
288
|
handler: async ({ name }, ctx) => {
|
|
266
289
|
const skill = session.skills.byName(name);
|
|
267
290
|
if (!skill) {
|
|
@@ -307,6 +330,23 @@ export function buildSynthesizeSkillPlugin(
|
|
|
307
330
|
// tool inherits the channel resolver's default, which denies in
|
|
308
331
|
// headless runs (the skill-author flow couldn't activate a new skill).
|
|
309
332
|
permission: { action: 'allow' },
|
|
333
|
+
// Rescans every skill source the boot loader used — the builtin and
|
|
334
|
+
// plugin dirs live wherever those packages are installed, so they're
|
|
335
|
+
// covered explicitly from the configured options.
|
|
336
|
+
isolation: {
|
|
337
|
+
capabilities: {
|
|
338
|
+
fs: {
|
|
339
|
+
read: [
|
|
340
|
+
userSkillsGlob,
|
|
341
|
+
projectSkillsGlob,
|
|
342
|
+
...(opts.builtinDir ? [`${opts.builtinDir}/**`] : []),
|
|
343
|
+
...(opts.pluginDirs ?? []).map((d) => `${d}/**`),
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
net: { mode: 'none' },
|
|
347
|
+
timeMs: 30_000,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
310
350
|
handler: async () => {
|
|
311
351
|
const { discoverSkills } = await import('./loader.js');
|
|
312
352
|
// Discover first, swap second: never empty the registry while
|
|
@@ -337,6 +377,8 @@ export function buildSynthesizeSkillPlugin(
|
|
|
337
377
|
name: z.string().min(1).describe('Exact tool name from the "Loadable tools" index.'),
|
|
338
378
|
}),
|
|
339
379
|
permission: { action: 'allow' },
|
|
380
|
+
// Registry lookup — the schema inclusion happens on later requests.
|
|
381
|
+
isolation: { capabilities: { net: { mode: 'none' }, timeMs: 10_000 } },
|
|
340
382
|
handler: ({ name }) => {
|
|
341
383
|
// The call itself is recorded in the log; `applyLazyTools` reads that
|
|
342
384
|
// to include the tool's schema on subsequent requests. Here we just
|
|
@@ -468,6 +468,10 @@ function buildChildContext(
|
|
|
468
468
|
signal: parentSignal, // child cancels when parent cancels
|
|
469
469
|
maxIterations: spec.maxIterations ?? 50,
|
|
470
470
|
subagents: spawner,
|
|
471
|
+
// Disarms turn-end checkpoints inside children (runReactLoop's recursion
|
|
472
|
+
// backstop) — a checkpoint spawning a child in a checkpoint-bearing mode
|
|
473
|
+
// must not gate the child's turn-end and recurse forever.
|
|
474
|
+
isSubagent: true,
|
|
471
475
|
emit: (event: EmittedEvent): Promise<MoxxyEvent> => childLog.append(event),
|
|
472
476
|
};
|
|
473
477
|
}
|