@muretai/agent-entry 1.9.0 → 1.11.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@muretai/agent-entry",
3
- "version": "1.9.0",
4
- "description": "Make your website answer AI agents: an A2A agent endpoint that verifies who is knocking, opens an account for them and replies signed, in one HTTP round trip. Zero dependencies. Pairs with llms.txt and WebMCP.",
3
+ "version": "1.11.0",
4
+ "description": "Agents already visit your site. Give them a way to become a customer: know who knocked, remember them next time, answer in the same request. Zero dependencies.",
5
5
  "type": "module",
6
6
  "main": "muretai-agent-entry.mjs",
7
7
  "exports": {
@@ -11,6 +11,8 @@
11
11
  "muretai-agent-entry.mjs",
12
12
  "conformance/",
13
13
  "examples/server.mjs",
14
+ "diagrams/",
15
+ "scripts/distill/",
14
16
  "LICENSE",
15
17
  "spec/",
16
18
  "README.md"
@@ -20,7 +22,10 @@
20
22
  },
21
23
  "scripts": {
22
24
  "test": "node conformance/run.mjs",
23
- "example": "node examples/server.mjs"
25
+ "example": "node examples/server.mjs",
26
+ "distill": "node scripts/distill/loop.mjs",
27
+ "distill:record": "node scripts/distill/record.mjs",
28
+ "distill:measure": "node scripts/distill/measure.mjs"
24
29
  },
25
30
  "keywords": [
26
31
  "ai-agent",
@@ -0,0 +1,32 @@
1
+ # Local feedback loop (this machine only)
2
+
3
+ Turn knocks you already answered into a **proposed** `skills[]` menu, and
4
+ measure whether that menu would have made the next visitor’s first knock
5
+ useful.
6
+
7
+ Nothing here uploads visitor text, DIDs, or traces. The Distiller is not
8
+ imported by `muretai-agent-entry.mjs`.
9
+
10
+ ```
11
+ npm run distill # fixtures → skills.json → holdout measure
12
+ npm run distill -- --m0 # also run the door conformance suite
13
+ npm run distill:record # print the observer snippet
14
+ ```
15
+
16
+ ## Record (optional)
17
+
18
+ `createAgentEntry({ observer: fileSink() })` appends POST outcomes to
19
+ `var/traces.jsonl` (gitignored). Card/notice GETs are dropped. DIDs are
20
+ not written. The door still discards the observer’s return — a full disk
21
+ cannot change a signed reply.
22
+
23
+ ## Admission
24
+
25
+ Publish the proposed menu onto *your* card only when:
26
+
27
+ 1. holdout first-knock useful rises
28
+ 2. emptying the Distiller kills that lift
29
+ 3. every proposed `examples[]` entry is answerable by *your* responder
30
+ 4. the door conformance suite is still green
31
+
32
+ This package never patches a stranger’s `skills[]`.
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Traces → generated/skills.json + rules.json.
4
+ * Empty Distiller: --empty (mutation). Never writes the running card.
5
+ */
6
+ import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
7
+ import { dirname, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { inferRules, loadCases, proposedSkills, readJsonl, tracesFromCases, respond } from './lib.mjs';
10
+
11
+ const HERE = dirname(fileURLToPath(import.meta.url));
12
+ const ROOT = resolve(HERE, '../..');
13
+ const OUT = resolve(ROOT, 'generated');
14
+
15
+ function localTraces(path) {
16
+ return readJsonl(path).map((row, i) => {
17
+ const text = row.text || row.doc?.text || '';
18
+ const env = respond(text);
19
+ return {
20
+ id: row.id || `local-${i + 1}`,
21
+ intent: /\b(book|reserve|slot)\b/i.test(text) ? 'book' : (/\b(price|cost|how much)\b/i.test(text) ? 'price' : 'other'),
22
+ text,
23
+ tag: env.tag,
24
+ environment_useful: env.useful,
25
+ };
26
+ });
27
+ }
28
+
29
+ const empty = process.argv.includes('--empty');
30
+ const tracesPath = process.env.AE_TRACES || resolve(ROOT, 'var/traces.jsonl');
31
+ const train = tracesFromCases(loadCases(), 'train');
32
+ const local = existsSync(tracesPath) ? localTraces(tracesPath) : [];
33
+ const rules = empty ? { empty: true, examples: [], evidence: [] } : inferRules([...train, ...local]);
34
+ const skills = proposedSkills(rules);
35
+
36
+ mkdirSync(OUT, { recursive: true });
37
+ writeFileSync(resolve(OUT, 'rules.json'), JSON.stringify(rules, null, 2) + '\n');
38
+ writeFileSync(resolve(OUT, 'skills.json'), JSON.stringify(skills, null, 2) + '\n');
39
+ writeFileSync(resolve(OUT, 'R.json'), JSON.stringify({
40
+ traces: train.length + local.length,
41
+ local: local.length,
42
+ empty,
43
+ book_require_day: Boolean(rules.book_require_day),
44
+ examples: rules.examples || [],
45
+ }, null, 2) + '\n');
46
+
47
+ process.stdout.write(
48
+ `distilled ${train.length} fixture + ${local.length} local traces`
49
+ + (empty ? ' (empty Distiller)' : '')
50
+ + ` → generated/skills.json (${skills.length} skill(s))\n`,
51
+ );
@@ -0,0 +1,10 @@
1
+ {
2
+ "comment": "First dojo: a booking desk that cannot answer a book request without a day. Gold is the fixture responder (same policy as examples/server.mjs). Placeholders only.",
3
+ "cases": [
4
+ { "id": "book-no-day", "split": "train", "intent": "book", "naive_text": "I want to book a shoot" },
5
+ { "id": "book-saturday", "split": "train", "intent": "book", "naive_text": "Book Saturday 14:00" },
6
+ { "id": "price", "split": "train", "intent": "price", "naive_text": "What does a 60-minute shoot cost?" },
7
+ { "id": "book-holdout", "split": "holdout", "intent": "book", "naive_text": "Can I reserve a session" },
8
+ { "id": "price-holdout", "split": "holdout", "intent": "price", "naive_text": "How much is a half-day?" }
9
+ ]
10
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Local AE distill helpers. The Distiller never imports the door runtime.
3
+ * Gold labels come from the fixture responder (the same policy the example
4
+ * studio already implements: a book request without a day is not yet useful).
5
+ */
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const HERE = dirname(fileURLToPath(import.meta.url));
11
+ const DAY = /\b(mon|tue|wed|thu|fri|sat|sun|monday|tuesday|wednesday|thursday|friday|saturday|sunday|20\d{2}-\d{2}-\d{2})\b/i;
12
+ const PRICE = /\b(price|cost|how much|jpy|half-day|60-minute)\b/i;
13
+
14
+ export function loadCases() {
15
+ return JSON.parse(readFileSync(join(HERE, 'fixtures.json'), 'utf8')).cases;
16
+ }
17
+
18
+ /** Environment: what this desk actually answers. */
19
+ export function respond(text) {
20
+ const t = String(text || '');
21
+ if (PRICE.test(t)) {
22
+ return { tag: 'price', useful: true, text: 'A 60-minute shoot is 12000 JPY, two people included.' };
23
+ }
24
+ if (DAY.test(t)) {
25
+ return { tag: 'open', useful: true, text: 'Saturday 14:00 is open. 12000 JPY for a 60-minute shoot.' };
26
+ }
27
+ return { tag: 'ask_day', useful: false, text: 'Which day? Name a weekday or YYYY-MM-DD.' };
28
+ }
29
+
30
+ export function naiveText(case_) {
31
+ return case_.naive_text;
32
+ }
33
+
34
+ export function inferRules(traces) {
35
+ const rules = {
36
+ empty: traces.length === 0,
37
+ book_require_day: false,
38
+ examples: [],
39
+ evidence: [],
40
+ };
41
+ for (const tr of traces) {
42
+ rules.evidence.push({ id: tr.id, intent: tr.intent, useful: tr.environment_useful, tag: tr.tag });
43
+ if (tr.intent === 'book' && !tr.environment_useful && tr.tag === 'ask_day') {
44
+ rules.book_require_day = true;
45
+ }
46
+ if (tr.environment_useful && tr.intent === 'book' && DAY.test(tr.text)) {
47
+ rules.examples.push(tr.text);
48
+ }
49
+ if (tr.environment_useful && tr.intent === 'price') {
50
+ rules.examples.push(tr.text);
51
+ }
52
+ }
53
+ rules.examples = [...new Set(rules.examples)];
54
+ return rules;
55
+ }
56
+
57
+ export function applyText(case_, rules) {
58
+ if (rules.empty) return naiveText(case_);
59
+ let text = naiveText(case_);
60
+ if (rules.book_require_day && case_.intent === 'book' && !DAY.test(text)) {
61
+ const ex = rules.examples.find((e) => DAY.test(e));
62
+ text = ex || `${text} Saturday`;
63
+ }
64
+ return text;
65
+ }
66
+
67
+ export function tracesFromCases(cases, split = 'train') {
68
+ return cases.filter((c) => c.split === split).map((c) => {
69
+ const text = naiveText(c);
70
+ const env = respond(text);
71
+ return {
72
+ id: c.id,
73
+ intent: c.intent,
74
+ text,
75
+ tag: env.tag,
76
+ environment_useful: env.useful,
77
+ };
78
+ });
79
+ }
80
+
81
+ export function proposedSkills(rules) {
82
+ if (rules.empty) return [];
83
+ const examples = rules.examples.length
84
+ ? rules.examples
85
+ : (rules.book_require_day ? ['Book Saturday 14:00'] : []);
86
+ return [{
87
+ id: 'ask',
88
+ name: 'signed-answers-about-the-studio',
89
+ description: rules.book_require_day
90
+ ? 'Ask what a shoot costs, and book by naming a day. The answer comes back signed.'
91
+ : 'Ask what a shoot costs and how to book. The answer comes back signed.',
92
+ tags: ['studio', 'booking', 'signed', 'inline-reply'],
93
+ examples,
94
+ }];
95
+ }
96
+
97
+ export function readJsonl(path) {
98
+ if (!existsSync(path)) return [];
99
+ return readFileSync(path, 'utf8').split('\n').filter(Boolean).map((line) => JSON.parse(line));
100
+ }
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Local loop: distill fixtures (+ optional var/traces.jsonl) → measure → mutation.
4
+ * --m0 also runs the door conformance suite. Never uploads. Never edits the card.
5
+ */
6
+ import { spawnSync } from 'node:child_process';
7
+ import { dirname, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const HERE = dirname(fileURLToPath(import.meta.url));
11
+ const ROOT = resolve(HERE, '../..');
12
+ const runM0 = process.argv.includes('--m0');
13
+
14
+ function run(file, extra = []) {
15
+ const r = spawnSync(process.execPath, [resolve(HERE, file), ...extra], {
16
+ cwd: ROOT,
17
+ encoding: 'utf8',
18
+ stdio: ['ignore', 'pipe', 'pipe'],
19
+ });
20
+ if (r.stdout) process.stdout.write(r.stdout);
21
+ if (r.stderr) process.stderr.write(r.stderr);
22
+ if (r.status !== 0) process.exit(r.status ?? 1);
23
+ }
24
+
25
+ run('distill.mjs');
26
+ run('measure.mjs');
27
+ process.stdout.write('\n--- mutation (empty Distiller) ---\n');
28
+ run('distill.mjs', ['--empty']);
29
+ run('measure.mjs', ['--empty']);
30
+ run('distill.mjs');
31
+
32
+ const unit = spawnSync(process.execPath, ['--test', resolve(HERE, 'test.mjs')], {
33
+ cwd: ROOT,
34
+ encoding: 'utf8',
35
+ });
36
+ if (unit.stdout) process.stdout.write(unit.stdout);
37
+ if (unit.stderr) process.stderr.write(unit.stderr);
38
+ if (unit.status !== 0) process.exit(unit.status ?? 1);
39
+
40
+ if (runM0) {
41
+ process.stdout.write('\n--- M0 npm test (conformance) ---\n');
42
+ const t = spawnSync('npm', ['test'], { cwd: ROOT, encoding: 'utf8' });
43
+ if (t.stdout) process.stdout.write(t.stdout);
44
+ if (t.stderr) process.stderr.write(t.stderr);
45
+ if (t.status !== 0) process.exit(t.status ?? 1);
46
+ }
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * M2: held-out visitor prompts. Naive sends the prompt as-is.
4
+ * Skill rewrites a book request that names no day using a distilled example.
5
+ * Gold = fixture responder. Mutation: --empty → apply == naive, lift 0.
6
+ */
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { dirname, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { applyText, loadCases, naiveText, respond } from './lib.mjs';
11
+
12
+ const HERE = dirname(fileURLToPath(import.meta.url));
13
+ const ROOT = resolve(HERE, '../..');
14
+ const OUT = resolve(ROOT, 'generated');
15
+
16
+ function loadRules(empty) {
17
+ if (empty) return { empty: true, examples: [] };
18
+ const p = resolve(OUT, 'rules.json');
19
+ if (!existsSync(p)) {
20
+ process.stderr.write('measure.mjs: run distill.mjs first (no generated/rules.json)\n');
21
+ process.exit(1);
22
+ }
23
+ return JSON.parse(readFileSync(p, 'utf8'));
24
+ }
25
+
26
+ function score(cases, textOf) {
27
+ const rows = [];
28
+ let ok = 0;
29
+ for (const c of cases) {
30
+ const text = textOf(c);
31
+ const env = respond(text);
32
+ if (env.useful) ok += 1;
33
+ rows.push({ id: c.id, intent: c.intent, text, useful: env.useful, tag: env.tag });
34
+ }
35
+ return { ok, n: cases.length, pct: cases.length ? ok / cases.length : 0, rows };
36
+ }
37
+
38
+ const empty = process.argv.includes('--empty');
39
+ const rules = loadRules(empty);
40
+ const holdout = loadCases().filter((c) => c.split === 'holdout');
41
+ const without = score(holdout, naiveText);
42
+ const withSkill = score(holdout, (c) => applyText(c, rules));
43
+ const lift = withSkill.pct - without.pct;
44
+
45
+ const lines = [
46
+ '# Agent Entry — local distill measure',
47
+ '',
48
+ empty ? 'Arm: **empty Distiller** (mutation).' : 'Arm: distilled `generated/rules.json`.',
49
+ '',
50
+ `| arm | holdout first-knock useful |`,
51
+ `|---|---|`,
52
+ `| without skill (send the prompt) | ${without.ok}/${without.n} (${(without.pct * 100).toFixed(0)}%) |`,
53
+ `| with skill | ${withSkill.ok}/${withSkill.n} (${(withSkill.pct * 100).toFixed(0)}%) |`,
54
+ `| lift | ${(lift * 100).toFixed(0)} pt |`,
55
+ '',
56
+ 'Gold is the fixture responder, not a transcript judge.',
57
+ '',
58
+ '### holdout',
59
+ '',
60
+ ...withSkill.rows.map((r) => {
61
+ const w = without.rows.find((x) => x.id === r.id);
62
+ return `- \`${r.id}\` naive="${w.text}" → ${w.tag} | skill="${r.text}" → ${r.tag} ${r.useful ? 'ok' : 'MISS'}`;
63
+ }),
64
+ '',
65
+ 'Proposed menu: generated/skills.json — not written onto the running card.',
66
+ '',
67
+ ];
68
+
69
+ mkdirSync(OUT, { recursive: true });
70
+ writeFileSync(resolve(OUT, 'report.md'), lines.join('\n'));
71
+ process.stdout.write(lines.join('\n'));
72
+
73
+ if (empty && lift !== 0) {
74
+ process.stderr.write('mutation failed: empty Distiller still produced lift\n');
75
+ process.exit(2);
76
+ }
77
+ if (!empty && lift <= 0) {
78
+ process.stderr.write('no lift on holdout — do not publish the proposed skills[]\n');
79
+ process.exit(2);
80
+ }
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Owner-local observer sink. Writes to a path on THIS machine. Never uploads.
4
+ *
5
+ * Wire it when you construct the door:
6
+ *
7
+ * import { fileSink } from './scripts/distill/record.mjs';
8
+ * createAgentEntry({ ..., observer: fileSink() });
9
+ *
10
+ * CLI: print the snippet, or append a JSON row from stdin.
11
+ *
12
+ * node scripts/distill/record.mjs
13
+ * node scripts/distill/record.mjs --file var/traces.jsonl --stdin < row.json
14
+ */
15
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
16
+ import { dirname, resolve } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ const HERE = dirname(fileURLToPath(import.meta.url));
20
+ const ROOT = resolve(HERE, '../..');
21
+
22
+ export function defaultTracePath() {
23
+ return process.env.AE_TRACES || resolve(ROOT, 'var/traces.jsonl');
24
+ }
25
+
26
+ /**
27
+ * Observer that cannot change a verdict (the door already discards its return).
28
+ * Drops card/notice GETs. Never writes a DID.
29
+ */
30
+ export function fileSink(path = defaultTracePath()) {
31
+ return (env) => {
32
+ try {
33
+ if (!env || env.stage === 'card_get' || env.stage === 'notice_get') return;
34
+ const row = {
35
+ ts: new Date().toISOString(),
36
+ text: typeof env.text === 'string' ? env.text : '',
37
+ verified: Boolean(env.verified),
38
+ stage: env.stage || null,
39
+ refuse_code: env.refuse_code ?? null,
40
+ };
41
+ mkdirSync(dirname(path), { recursive: true });
42
+ appendFileSync(path, JSON.stringify(row) + '\n');
43
+ } catch {
44
+ // Observer contract: a throw is swallowed by the door; we swallow here too.
45
+ }
46
+ };
47
+ }
48
+
49
+ function usage(code = 0) {
50
+ process.stdout.write(
51
+ 'Local AE traces — this machine only, never uploaded.\n'
52
+ + '\n'
53
+ + ' import { fileSink } from \'./scripts/distill/record.mjs\';\n'
54
+ + ' createAgentEntry({ seedHex, name, baseUrl, responder, observer: fileSink() });\n'
55
+ + '\n'
56
+ + ' node scripts/distill/record.mjs --stdin < row.json\n'
57
+ + ` default file: ${defaultTracePath()}\n`,
58
+ );
59
+ process.exit(code);
60
+ }
61
+
62
+ if (import.meta.url === `file://${process.argv[1]}` || process.argv[1]?.endsWith('record.mjs')) {
63
+ const argv = process.argv.slice(2);
64
+ if (!argv.length || argv.includes('--help') || argv.includes('-h')) usage(0);
65
+ let file = defaultTracePath();
66
+ let stdin = false;
67
+ for (let i = 0; i < argv.length; i++) {
68
+ if (argv[i] === '--file') { file = resolve(argv[++i] || ''); continue; }
69
+ if (argv[i] === '--stdin') { stdin = true; continue; }
70
+ usage(1);
71
+ }
72
+ if (!stdin) usage(1);
73
+ const raw = readFileSync(0, 'utf8');
74
+ if (!raw.trim()) {
75
+ process.stderr.write('record.mjs: stdin is empty\n');
76
+ process.exit(1);
77
+ }
78
+ let doc;
79
+ try { doc = JSON.parse(raw); } catch (e) {
80
+ process.stderr.write(`record.mjs: stdin is not JSON: ${e.message}\n`);
81
+ process.exit(1);
82
+ }
83
+ fileSink(file)({
84
+ text: doc.text || '',
85
+ verified: Boolean(doc.verified),
86
+ stage: doc.stage || 'signed_post',
87
+ refuse_code: doc.refuse_code ?? null,
88
+ });
89
+ process.stdout.write(`recorded 1 row → ${file}\n`);
90
+ }
@@ -0,0 +1,43 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { applyText, inferRules, loadCases, naiveText, proposedSkills, respond, tracesFromCases } from './lib.mjs';
4
+
5
+ function score(cases, textOf) {
6
+ let ok = 0;
7
+ for (const c of cases) {
8
+ if (respond(textOf(c)).useful) ok += 1;
9
+ }
10
+ return cases.length ? ok / cases.length : 0;
11
+ }
12
+
13
+ test('responder asks for a day when the book prompt names none', () => {
14
+ assert.equal(respond('I want to book a shoot').useful, false);
15
+ assert.equal(respond('I want to book a shoot').tag, 'ask_day');
16
+ assert.equal(respond('Book Saturday 14:00').useful, true);
17
+ });
18
+
19
+ test('distilled menu beats sending the prompt as-is on holdout', () => {
20
+ const cases = loadCases();
21
+ const rules = inferRules(tracesFromCases(cases, 'train'));
22
+ const holdout = cases.filter((c) => c.split === 'holdout');
23
+ const without = score(holdout, naiveText);
24
+ const withSkill = score(holdout, (c) => applyText(c, rules));
25
+ assert.ok(withSkill > without, `expected lift, got without=${without} with=${withSkill}`);
26
+ assert.equal(withSkill, 1);
27
+ });
28
+
29
+ test('empty Distiller produces no lift (producer mutation)', () => {
30
+ const holdout = loadCases().filter((c) => c.split === 'holdout');
31
+ const without = score(holdout, naiveText);
32
+ const withEmpty = score(holdout, (c) => applyText(c, { empty: true }));
33
+ assert.equal(withEmpty, without);
34
+ });
35
+
36
+ test('proposed skills[] examples are answerable by the responder', () => {
37
+ const rules = inferRules(tracesFromCases(loadCases(), 'train'));
38
+ const skills = proposedSkills(rules);
39
+ assert.ok(skills.length >= 1);
40
+ for (const ex of skills[0].examples) {
41
+ assert.equal(respond(ex).useful, true, `example not answerable: ${ex}`);
42
+ }
43
+ });