@danceiny/gotry 0.0.1-rc.7 → 0.0.1-rc.8

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.
Files changed (44) hide show
  1. package/README.md +27 -74
  2. package/bin/gotry-inner.js +50 -1
  3. package/bin/gotry-stdio-ask.js +59 -0
  4. package/cordis.gotry-patch.yml +56 -5
  5. package/data/flights_2026.json +7 -4
  6. package/data/hotels_2026.json +102 -15
  7. package/data/time-slot-eval.json +384 -0
  8. package/data/yunnan-pack.json +2 -1
  9. package/dist/capabilities/agent-reach-bridge.py +90 -0
  10. package/dist/capabilities/anything.js +2 -2
  11. package/dist/capabilities/hbcli.js +11 -6
  12. package/dist/capabilities/incident-log.js +2 -1
  13. package/dist/scripts/agent-reach-tests.js +1 -1
  14. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  15. package/dist/scripts/hbcli-tests.js +18 -2
  16. package/dist/scripts/memory-capture-tests.js +77 -0
  17. package/dist/scripts/memory-metrics.js +38 -0
  18. package/dist/scripts/nudge-digest.js +93 -0
  19. package/dist/scripts/probe-poi-tests.js +9 -0
  20. package/dist/scripts/replay.js +73 -1
  21. package/dist/scripts/skeleton-check.js +3 -1
  22. package/dist/scripts/skills-contract-tests.js +85 -0
  23. package/dist/scripts/smoke.js +201 -3
  24. package/dist/scripts/time-eval-tests.js +318 -0
  25. package/dist/src/dsh-llm.js +27 -7
  26. package/dist/src/index.js +342 -92
  27. package/dist/src/loop.js +37 -10
  28. package/dist/src/memory-capture.js +40 -0
  29. package/dist/src/memory-utility.js +55 -0
  30. package/dist/src/mock-llm.js +6 -1
  31. package/dist/src/slot-spec.js +165 -0
  32. package/dist/src/time-anchor.js +100 -0
  33. package/dist/src/tool-packet.js +12 -0
  34. package/dist/src/travel-slots.js +144 -0
  35. package/dist/src/wish-pool.js +27 -0
  36. package/package.json +6 -2
  37. package/ts/capabilities/hbcli.ts +6 -6
  38. package/ts/capabilities/incident-log.ts +6 -3
  39. package/ts/scripts/skeleton-check.ts +5 -1
  40. package/ts/src/dsh-llm.ts +27 -7
  41. package/ts/src/index.ts +292 -77
  42. package/ts/src/loop.ts +52 -17
  43. package/ts/src/mock-llm.ts +13 -1
  44. package/ts/cordis.gotry-patch.yml +0 -36
@@ -0,0 +1,77 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mergeProfile } from '../src/memory-capture.js';
3
+ const cur = {
4
+ weights: {
5
+ escape_rest: 1
6
+ },
7
+ evidence: [
8
+ '用户原话:「想去湖边什么都不干」'
9
+ ],
10
+ hard: {
11
+ wake_not_before: '07:00'
12
+ }
13
+ };
14
+ {
15
+ const m1 = mergeProfile(cur, {
16
+ evidence: [
17
+ '用户原话:「预算 5000 以内」',
18
+ '用户原话:「想去湖边什么都不干」'
19
+ ]
20
+ });
21
+ assert.equal(m1?.evidence.length, 2, `追加新+跳过重复,实际 ${m1?.evidence.length}`);
22
+ assert.ok(m1?.evidence[0].includes('湖边'), '既有 evidence 原位保留(P0)');
23
+ const m2 = mergeProfile(m1, {
24
+ evidence: [
25
+ '用户原话:「预算 5000 以内」'
26
+ ]
27
+ });
28
+ assert.equal(m2, null, '纯重复补丁应返 null(幂等)');
29
+ console.log('1. 追加不删史 + 幂等 OK');
30
+ }{
31
+ const m = mergeProfile(cur, {
32
+ weights: {
33
+ escape_rest: 0.5,
34
+ curiosity: 0.5
35
+ },
36
+ evidence: [
37
+ '用户原话:「这次既要躺平也要探索」'
38
+ ]
39
+ });
40
+ const sum = Object.values(m?.weights ?? {}).reduce((a, b)=>a + b, 0);
41
+ assert.ok(Math.abs(sum - 1) < 0.01, `权重应归一,实际 sum=${sum}`);
42
+ const noEv = mergeProfile(cur, {
43
+ weights: {
44
+ escape_rest: 0.3,
45
+ curiosity: 0.7
46
+ }
47
+ });
48
+ assert.equal(noEv?.weights.escape_rest, 1, '权重变更无新 evidence 应被拒(P0)');
49
+ console.log('2. 权重归一 + P0 证据校验 OK');
50
+ }{
51
+ const first = mergeProfile(null, {
52
+ weights: {
53
+ escape_rest: 1
54
+ },
55
+ evidence: [
56
+ '用户原话:「想去湖边」'
57
+ ]
58
+ });
59
+ assert.ok(first && first.evidence.length === 1 && Object.keys(first.weights).length === 1, '首存应生效');
60
+ assert.equal(mergeProfile(null, null), null, 'null 补丁仍守卫');
61
+ console.log('2.5 首存语义(current=null→空档案) OK');
62
+ }{
63
+ assert.equal(mergeProfile(cur, null), null, 'null 补丁');
64
+ assert.equal(mergeProfile(cur, {}), null, '空补丁');
65
+ const m = mergeProfile(cur, {
66
+ hard: {
67
+ wake_not_before: '08:30',
68
+ budget_cny: 5000
69
+ }
70
+ });
71
+ assert.equal(m?.hard?.wake_not_before, '08:30', 'hard 后到优先');
72
+ assert.equal(m?.hard?.budget_cny, 5000, 'hard 新键并入');
73
+ console.log('3. 空守卫 + hard 覆盖后到优先 OK');
74
+ }console.log('\nMEMORY-MERGE TESTS: 3/3 OK(T1 合并守门层)');
75
+
76
+
77
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/memory-capture-tests.ts
@@ -0,0 +1,38 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { projectUtility } from '../src/memory-utility.js';
4
+ const stateRoot = process.argv[2] ?? '.';
5
+ const stateDir = join(stateRoot, 'gotry-state');
6
+ function readJson(path, fallback) {
7
+ try {
8
+ return JSON.parse(readFileSync(path, 'utf-8'));
9
+ } catch {
10
+ return fallback;
11
+ }
12
+ }
13
+ function readJsonl(path) {
14
+ try {
15
+ return readFileSync(path, 'utf-8').split('\n').filter(Boolean).map((l)=>JSON.parse(l));
16
+ } catch {
17
+ return [];
18
+ }
19
+ }
20
+ const pool = readJson(join(stateDir, 'wish-pool.json'), []);
21
+ const events = readJsonl(join(stateDir, 'memory-utility.jsonl'));
22
+ const projection = projectUtility(events);
23
+ const active = pool.filter((w)=>typeof w.wish_id === 'string');
24
+ const recalledWishes = Object.values(projection).filter((w)=>w.recalled > 0).length;
25
+ const verifiedWishes = Object.values(projection).filter((w)=>w.verified > 0).length;
26
+ const refluxBaseline = verifiedWishes / Math.max(recalledWishes, 1);
27
+ console.log('=== GoTry 记忆效用指标(M4 北极星过程面,只读) ===');
28
+ console.log(`wish pool: ${pool.length} 条在册(其中 ${pool.filter((w)=>w.muted).length} 条休眠,${active.length} 条有稳定 wish_id)`);
29
+ console.log(`效用事件: ${events.length} 条(recalled=${events.filter((e)=>e.kind === 'recalled').length}, applied=${events.filter((e)=>e.kind === 'applied').length}, verified=${events.filter((e)=>e.kind === 'verified_outcome').length})`);
30
+ for (const w of Object.values(projection)){
31
+ const name = pool.find((p)=>p.wish_id === w.wish_id)?.name ?? w.wish_id;
32
+ console.log(` - ${name}: status=${w.status}, recalled=${w.recalled}, applied=${w.applied}, verified=${w.verified}`);
33
+ }
34
+ console.log(`经验回流率基线 = ${verifiedWishes}/${recalledWishes} = ${refluxBaseline.toFixed(2)}(verified/recalled;单用户起步期样本稀疏属预期)`);
35
+ if (pool.length === 0) console.log('(wish pool 为空——首访用户,指标从首条憧憬入池开始积累)');
36
+
37
+
38
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/memory-metrics.ts
@@ -0,0 +1,93 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { pickNudgeWish } from '../src/wish-pool.js';
4
+ import { projectUtility } from '../src/memory-utility.js';
5
+ import { buildTimeAnchor } from '../src/time-anchor.js';
6
+ function arg(name) {
7
+ const i = process.argv.indexOf(name);
8
+ return i >= 0 ? process.argv[i + 1] : undefined;
9
+ }
10
+ const stateRoot = arg('--state-root') ?? '.';
11
+ const stateDir = join(stateRoot, 'gotry-state');
12
+ function readJson(path, fallback) {
13
+ try {
14
+ return JSON.parse(readFileSync(path, 'utf-8'));
15
+ } catch {
16
+ return fallback;
17
+ }
18
+ }
19
+ function readEvents() {
20
+ try {
21
+ return readFileSync(join(stateDir, 'memory-utility.jsonl'), 'utf-8').split('\n').filter(Boolean).map((l)=>JSON.parse(l));
22
+ } catch {
23
+ return [];
24
+ }
25
+ }
26
+ if (process.env['GOTRY_NUDGE_ENABLED'] === 'false') {
27
+ console.log('回访已关闭(GOTRY_NUDGE_ENABLED=false)——可关闭契约,正常退出');
28
+ process.exit(0);
29
+ }
30
+ const pool = readJson(join(stateDir, 'wish-pool.json'), []);
31
+ const anchor = buildTimeAnchor(new Date());
32
+ const ctx = {
33
+ days: arg('--days') ? Number(arg('--days')) : undefined,
34
+ budgetCny: arg('--budget') ? Number(arg('--budget')) : undefined,
35
+ month: arg('--month') ? Number(arg('--month')) : new Date().getMonth() + 1
36
+ };
37
+ const match = pickNudgeWish(pool, ctx);
38
+ const lines = [
39
+ `# 「下一次出发」回访摘要(${anchor.today} ${anchor.todayWeekdayZh})`
40
+ ];
41
+ if (!match) {
42
+ lines.push(`在册 ${pool.filter((p)=>!p.muted).length} 条憧憬,当前窗口无可成行匹配——不打扰(0..1 纪律:不硬推)。`);
43
+ } else {
44
+ const c = match.entry.conditions ?? {};
45
+ const u = projectUtility(readEvents())[match.wishId];
46
+ lines.push(`**${String(match.entry.name ?? match.wishId)}**(${match.hits.join(' + ')},命中 ${match.score}/3 项)`);
47
+ lines.push(`- 成行条件: ${JSON.stringify(c)}`);
48
+ if (match.entry.reason) lines.push(`- 当初为什么: ${String(match.entry.reason).slice(0, 160)}`);
49
+ lines.push(`- 效用状态: ${u?.status ?? 'unknown'}(被召回 ${u?.recalled ?? 0} 次)`);
50
+ lines.push('');
51
+ lines.push('_想安排就说一声;不想被打扰说「mute <名字>」即可休眠,憧憬不被拒绝。_');
52
+ }
53
+ const digest = lines.join('\n');
54
+ const channel = process.env['GOTRY_NUDGE_CHANNEL'] ?? 'stdout';
55
+ if (channel === 'file') {
56
+ const out = process.env['GOTRY_NUDGE_FILE'] ?? join(stateDir, 'nudge-digest.md');
57
+ mkdirSync(dirname(out), {
58
+ recursive: true
59
+ });
60
+ writeFileSync(out, digest + '\n', 'utf-8');
61
+ console.log(`摘要已写入 ${out}`);
62
+ } else if (channel === 'lark') {
63
+ const webhook = process.env['GOTRY_LARK_WEBHOOK'];
64
+ if (!webhook) {
65
+ console.log('lark 通道未配置(GOTRY_LARK_WEBHOOK 缺失)——降级 stdout,即插即用等 key:');
66
+ console.log(digest);
67
+ } else {
68
+ try {
69
+ const resp = await fetch(webhook, {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/json'
73
+ },
74
+ body: JSON.stringify({
75
+ msg_type: 'text',
76
+ content: {
77
+ text: digest
78
+ }
79
+ })
80
+ });
81
+ console.log(`lark 投递 HTTP ${resp.status}`);
82
+ if (!resp.ok) console.log(digest);
83
+ } catch (e) {
84
+ console.log(`lark 投递失败(${e.message})——降级 stdout:`);
85
+ console.log(digest);
86
+ }
87
+ }
88
+ } else {
89
+ console.log(digest);
90
+ }
91
+
92
+
93
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/nudge-digest.ts
@@ -73,6 +73,15 @@ pass('6. 不抛错 + 返回类型是 string 或 null(边界)', ()=>{
73
73
  }
74
74
  }
75
75
  });
76
+ pass('7. 金标准对话噪音回归(2026-08-28 巡检:访谈答案/多段首查不再误触发)', ()=>{
77
+ assert.equal(probePoi('我的工作时间是UTC+4的早上10点到下午7点'), null, '访谈答案(>12 字含陈述动词)不触发');
78
+ assert.equal(probePoi('明天有空'), null, '短句含陈述动词不触发');
79
+ assert.equal(probePoi('7.17周五22:40落地深圳,7.18早上去香港办银行开户&保险签约;争取7.18当天飞泰国普吉岛……8.10周一凌晨从深圳起飞,周一上班前到迪拜。请给我做机票和酒店的行程规划和推荐。'), null, '多段首查无 POI 信号不触发');
80
+ const booking = probePoi('我订了酒店:The Title East Wing Rawai,7.18入住 7.23 退房');
81
+ if (!booking || !booking.includes('Title')) throw new Error(`订酒店应抓名称段,得 ${JSON.stringify(booking)}`);
82
+ console.log(' 7a. 访谈答案/多段首查 → null OK');
83
+ console.log(' 7b. 订酒店抓名称段(The Title…) OK');
84
+ });
76
85
  console.log('\nprobePoi TESTS: 6 类 OK(不崩 + 5 类触发 + 1 类不触发返 null)');
77
86
 
78
87
 
@@ -39,6 +39,78 @@ if (!state.profile.workWindow?.evidence) throw new Error('FAIL: workWindow 缺
39
39
  if (!state.profile.bookedResources?.length) throw new Error('FAIL: bookedResources 缺失');
40
40
  if (!(missing.length === 1 && missing[0] === 'budgetTier')) throw new Error(`FAIL: 剩余待问应为 [budgetTier],实为 [${missing.join(',')}]`);
41
41
  console.log('REPLAY ASSERTS OK');
42
-
42
+ {
43
+ const conflictScript = [
44
+ {
45
+ when: '重新算一下',
46
+ extraction: {
47
+ schema_version: 'travel_slot_extraction.v1',
48
+ language: 'zh',
49
+ domains: [
50
+ 'requisition'
51
+ ],
52
+ slots: {
53
+ requisition: {
54
+ mode: 'create',
55
+ destination: '普吉岛',
56
+ start_date: '2026-12-25',
57
+ trip_type: 'round_trip'
58
+ }
59
+ },
60
+ missing_slots: []
61
+ }
62
+ }
63
+ ];
64
+ const solvePort = (spec)=>{
65
+ spec.skeletonHub = true;
66
+ return solveUnified(spec);
67
+ };
68
+ const base = createMockLlm(join('..', 'data', 'flights_2026.json'), conflictScript);
69
+ const singleSpecLlm = {
70
+ ...base,
71
+ extractSpec: async ()=>({
72
+ segments: [
73
+ {
74
+ id: 's1',
75
+ role: 'choice',
76
+ date: '2026-07-01',
77
+ options: [
78
+ {
79
+ id: 'o1',
80
+ label: 'o1',
81
+ move: {
82
+ hub: 'SZX',
83
+ services: [
84
+ {
85
+ id: 'f1',
86
+ depMin: 600,
87
+ arrMin: 700
88
+ }
89
+ ],
90
+ bufferMin: 60,
91
+ originTransferMin: 30,
92
+ destTransferMin: 30
93
+ }
94
+ }
95
+ ]
96
+ }
97
+ ]
98
+ })
99
+ };
100
+ const state1 = structuredClone(state);
101
+ const solveBefore = state1.solve;
102
+ const r1 = await runTurn(state1, '12月25日出发,重新算一下', singleSpecLlm, [
103
+ ...history
104
+ ], solvePort);
105
+ if (!r1.reply.includes('日期分歧')) throw new Error(`FAIL: 单日期段分歧应被闸拦下,实际:${r1.reply.slice(0, 160)}`);
106
+ if (r1.state.solve !== solveBefore) throw new Error('FAIL: 分歧时不应求解(spec 日期未确认)');
107
+ const state2 = structuredClone(state);
108
+ const r2 = await runTurn(state2, '重新算一下', base, [
109
+ ...history
110
+ ], solvePort);
111
+ if (r2.reply.includes('日期分歧')) throw new Error(`FAIL: 多段行程应旁路日期闸(无逐段真值不判),实际:${r2.reply.slice(0, 160)}`);
112
+ if (!r2.state.solve) throw new Error('FAIL: 多段旁路后应正常求解');
113
+ console.log('SPEC-SLOT DATE GATE OK(单日期段分歧拦截 + 多段旁路不误伤)');
114
+ }
43
115
 
44
116
  //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/replay.ts
@@ -27,7 +27,9 @@ export async function checkConnectivity(a, b) {
27
27
  evidence: inHub ? `[骨架:openflights] ❌ ${a}↔${b} 枢纽间无直飞记录——引擎应将此候选降权或要求中转` : `[骨架:openflights] ○ ${a}或${b}不在枢纽集,骨架不覆盖(不作否定结论)`
28
28
  };
29
29
  }
30
- if (process.argv[2] && process.argv[3]) {
30
+ import { pathToFileURL } from 'node:url';
31
+ const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
32
+ if (isMain && process.argv[2] && process.argv[3]) {
31
33
  console.log((await checkConnectivity(process.argv[2], process.argv[3])).evidence);
32
34
  }
33
35
 
@@ -0,0 +1,85 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { apply } from '../src/index.js';
4
+ async function main() {
5
+ const registered = [];
6
+ apply({
7
+ tools: {
8
+ register: (t)=>registered.push(t)
9
+ }
10
+ }, {
11
+ stateRoot: '.',
12
+ timeoutMs: 1000,
13
+ hbcliBin: 'hbcli-not-on-path'
14
+ });
15
+ const desc = (n)=>{
16
+ const t = registered.find((x)=>x.name === n);
17
+ if (!t) return '';
18
+ const params = Object.values(t.parameters?.properties ?? {}).map((p)=>p.description ?? '').join(' ');
19
+ return `${t.description ?? ''} ${params}`;
20
+ };
21
+ const anythingDesc = desc('gotry_anything_search');
22
+ const hotelsDesc = desc('gotry_hotel_search');
23
+ let token = '';
24
+ try {
25
+ const out = execFileSync('git', [
26
+ 'credential-osxkeychain',
27
+ 'get'
28
+ ], {
29
+ input: 'protocol=https\nhost=github.com\n'
30
+ }).toString();
31
+ token = (out.match(/^password=(.+)$/m) ?? [
32
+ '',
33
+ ''
34
+ ])[1].trim();
35
+ } catch {}
36
+ if (!token) {
37
+ console.log('SKIP: 无 GitHub 凭证(契约对齐在有凭证环境跑)');
38
+ return;
39
+ }
40
+ const get = async (path)=>{
41
+ const res = await fetch(`https://api.github.com/repos/Danceiny/hotelbyte-skills/contents/${path}`, {
42
+ headers: {
43
+ Authorization: `Bearer ${token}`,
44
+ Accept: 'application/vnd.github+json'
45
+ },
46
+ signal: AbortSignal.timeout(15_000)
47
+ });
48
+ if (!res.ok) throw new Error(`${path}: HTTP ${res.status}`);
49
+ const j = await res.json();
50
+ return Buffer.from(j.content ?? '', j.encoding === 'base64' ? 'base64' : 'utf8').toString('utf8');
51
+ };
52
+ let anythingContract = '';
53
+ let hotelsContract = '';
54
+ try {
55
+ ;
56
+ [anythingContract, hotelsContract] = await Promise.all([
57
+ get('contracts/anything.md'),
58
+ get('contracts/hotels.md')
59
+ ]);
60
+ } catch (e) {
61
+ console.log(`SKIP: 契约拉取失败(${e.message})——对齐检查跳过,不红`);
62
+ return;
63
+ }
64
+ assert.ok(anythingContract.includes('contentType'), '契约应声明 contentType');
65
+ assert.ok(anythingDesc.includes('contentType'), 'gotry_anything_search 描述应含 contentType(契约对齐)');
66
+ assert.ok(anythingContract.includes('agent-reach'), '契约应声明与 agent-reach 的域边界');
67
+ assert.ok(/agent[-_]reach/.test(anythingDesc), 'gotry_anything_search 描述应引域边界(agent[-_]reach)');
68
+ for (const p of [
69
+ 'destination',
70
+ 'checkIn',
71
+ 'checkOut'
72
+ ]){
73
+ assert.ok(hotelsContract.includes(p), `契约应声明 ${p}`);
74
+ assert.ok(hotelsDesc.includes(p), `gotry_hotel_search 描述应含 ${p}(契约对齐)`);
75
+ }
76
+ assert.ok(anythingContract.includes('not-installed') || anythingContract.includes('三值'), '契约应声明三值降级');
77
+ console.log('SKILLS CONTRACT TESTS: 2/2 OK(anything/hotels 契约与 gotry 工具描述对齐)');
78
+ }
79
+ main().catch((e)=>{
80
+ console.error(e);
81
+ process.exitCode = 1;
82
+ });
83
+
84
+
85
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/skills-contract-tests.ts
@@ -3,17 +3,20 @@ import { join } from 'node:path';
3
3
  import { apply } from '../src/index.js';
4
4
  async function main() {
5
5
  const registered = [];
6
+ const variables = {};
6
7
  const ctx = {
7
8
  tools: {
8
9
  register: (t)=>registered.push(t)
10
+ },
11
+ systemPrompt: {
12
+ variable: (name, provider)=>{
13
+ variables[name] = provider;
14
+ }
9
15
  }
10
16
  };
11
17
  apply(ctx, {
12
- pythonBin: '../.venv/bin/python',
13
- pythonPath: '../py',
14
18
  stateRoot: '.',
15
19
  timeoutMs: 30_000,
16
- preferInProcess: true,
17
20
  hbcliBin: 'hbcli-not-on-path'
18
21
  });
19
22
  console.log(`registered tools: ${registered.map((t)=>t.name).join(', ')}`);
@@ -79,6 +82,201 @@ async function main() {
79
82
  }, null);
80
83
  console.log(`\nwish pool -> ${JSON.stringify(added)}`);
81
84
  if (result.recommended !== 'qiandao') throw new Error('FAIL: expected qiandao recommended');
85
+ if (typeof feasibility.presentResult !== 'function') throw new Error('FAIL: feasibility 缺 presentResult');
86
+ const view = feasibility.presentResult({
87
+ payload
88
+ }, result);
89
+ if (!view?.title?.includes('qiandao')) throw new Error(`FAIL: 结果卡标题缺推荐,实际 ${view?.title}`);
90
+ const body = view?.content?.[0]?.text ?? '';
91
+ if (!body.includes('✅') || !body.includes('¥')) throw new Error('FAIL: 结果卡缺判定行/成本行');
92
+ console.log(`\nresult card: ${view?.title}\n${body.split('\n').slice(0, 5).join('\n')}`);
93
+ for (const n of [
94
+ 'gotry_feasibility_check',
95
+ 'gotry_hotel_search',
96
+ 'gotry_weather_check',
97
+ 'gotry_anything_search',
98
+ 'gotry_agent_reach'
99
+ ]){
100
+ if (typeof byName(n).presentResult !== 'function') throw new Error(`FAIL: ${n} 缺 presentResult`);
101
+ }
102
+ const ar = byName('gotry_agent_reach');
103
+ const arView = ar.presentResult({
104
+ query: {
105
+ action: 'reach',
106
+ channel: 'v2ex',
107
+ method: 'get_hot_topics'
108
+ }
109
+ }, {
110
+ verdict: 'found',
111
+ summary: '10 topics'
112
+ });
113
+ if (!arView?.title?.includes('✅') || !arView.title.includes('v2ex.get_hot_topics')) throw new Error(`FAIL: agent_reach 结果卡,实际 ${arView?.title}`);
114
+ console.log(`result cards on 5 tools; agent_reach card: ${arView.title}`);
115
+ const ws = byName('gotry_web_search');
116
+ const r1 = await ws.execute({
117
+ query: 'not-a-url'
118
+ }, null);
119
+ const r2 = await ws.execute({
120
+ url: 'not-a-url'
121
+ }, null);
122
+ for (const [name, r] of [
123
+ [
124
+ 'string',
125
+ r1
126
+ ],
127
+ [
128
+ 'bare-obj',
129
+ r2
130
+ ]
131
+ ]){
132
+ if (r.summary === 'url 必填') throw new Error(`FAIL: ${name} 形态未被 unwrapQuery 接住`);
133
+ }
134
+ console.log('unwrapQuery: string + bare-object shapes both accepted');
135
+ const skTool = byName('gotry_skeleton_check');
136
+ const skFlat = await skTool.execute({
137
+ from: 'HKG',
138
+ to: 'BKK'
139
+ }, null);
140
+ if (skFlat.connected !== true) throw new Error(`FAIL: 骨架平铺调用应 connected=true,实际 ${JSON.stringify(skFlat).slice(0, 120)}`);
141
+ const skBad = await skTool.execute({
142
+ from: '',
143
+ to: ''
144
+ }, null);
145
+ if (typeof skBad?.summary === 'string' && skBad.connected === undefined) {
146
+ console.log('skeleton guard-fallback shape survives (loose schema)');
147
+ }
148
+ console.log(`skeleton flat-args: connected=${skFlat.connected}`);
149
+ {
150
+ const hotel = byName('gotry_hotel_search');
151
+ const resolved = await hotel.execute({
152
+ query: {
153
+ destination: '大理',
154
+ checkIn: '2026-9-4',
155
+ checkOut: '2026-09-06'
156
+ }
157
+ }, null);
158
+ if (!resolved.date_notes?.some((n)=>n.includes('2026-9-4 → 2026-09-04'))) {
159
+ throw new Error(`FAIL: 非规整 ISO 应产生 slot-resolved note,实际 ${JSON.stringify(resolved.date_notes)}`);
160
+ }
161
+ const unresolved = await hotel.execute({
162
+ query: {
163
+ destination: '大理',
164
+ checkIn: '近期'
165
+ }
166
+ }, null);
167
+ if (!unresolved.date_notes?.some((n)=>n.includes('日期未解析:近期'))) {
168
+ throw new Error(`FAIL: 词表外表达应产生「日期未解析」note(不猜),实际 ${JSON.stringify(unresolved.date_notes)}`);
169
+ }
170
+ console.log('hotel date slots: verbatim resolved in code layer; unresolved degrades with explicit note');
171
+ }
172
+ {
173
+ const wish = byName('gotry_wish_pool_add');
174
+ const w = await wish.execute({
175
+ entry: {
176
+ name: 'envelope-probe',
177
+ conditions: {
178
+ days: 3
179
+ }
180
+ }
181
+ }, null);
182
+ if (w.ok !== true) throw new Error(`FAIL: wish 添加应 ok:true,实际 ${JSON.stringify(w)}`);
183
+ if (result.ok !== true) throw new Error(`FAIL: feasibility 应 ok:true,实际 ${String(result.ok)}`);
184
+ console.log('observation envelope: success payloads carry flat ok:true (guard fallback = failure branch)');
185
+ }
186
+ {
187
+ const add = byName('gotry_wish_pool_add');
188
+ const list = byName('gotry_wish_pool_list');
189
+ const a = await add.execute({
190
+ entry: {
191
+ name: 'sidecar-probe-洱海',
192
+ reason: '5 天起',
193
+ conditions: {
194
+ days: 5,
195
+ budget_cny: 4950,
196
+ best_months: [
197
+ 3,
198
+ 4,
199
+ 5
200
+ ]
201
+ }
202
+ }
203
+ }, null);
204
+ if (!a.wish_id) throw new Error('FAIL: wish add 应返回稳定 wish_id');
205
+ const recall = await list.execute({
206
+ query: {
207
+ action: 'recall',
208
+ days: 6,
209
+ budgetCny: 6000,
210
+ month: 4
211
+ }
212
+ }, null);
213
+ if (!recall.suggestion?.wish_id) throw new Error(`FAIL: 条件命中应召回恰好 1 条(0..1),实际 ${JSON.stringify(recall.suggestion)}`);
214
+ if ((recall.suggestion.match_score ?? 0) < 3) throw new Error(`FAIL: 召回应按条件匹配评分,实际 ${JSON.stringify(recall.suggestion)}`);
215
+ const confirm = await list.execute({
216
+ query: {
217
+ action: 'confirm-outcome',
218
+ wishId: a.wish_id,
219
+ attribution: 'helpful',
220
+ detail: '用户明说:这条建议成了'
221
+ }
222
+ }, null);
223
+ if (!confirm.ok || confirm.status !== 'helpful') throw new Error(`FAIL: owner 确认归因应落盘,实际 ${JSON.stringify(confirm)}`);
224
+ const noAttr = await list.execute({
225
+ query: {
226
+ action: 'confirm-outcome',
227
+ wishId: a.wish_id
228
+ }
229
+ }, null);
230
+ if (noAttr.ok !== false) throw new Error('FAIL: 无归因的 confirm 应拒绝(归因不许缺省)');
231
+ await add.execute({
232
+ entry: {
233
+ name: 'sidecar-probe-洱海',
234
+ conditions: {
235
+ days: 5,
236
+ budget_cny: 4950,
237
+ best_months: [
238
+ 3,
239
+ 4,
240
+ 5
241
+ ]
242
+ },
243
+ muted: true
244
+ }
245
+ }, null);
246
+ const recall2 = await list.execute({
247
+ query: {
248
+ action: 'recall',
249
+ days: 6,
250
+ budgetCny: 6000,
251
+ month: 4
252
+ }
253
+ }, null);
254
+ if (recall2.suggestion?.wish_id === a.wish_id) throw new Error('FAIL: muted wish 不得召回');
255
+ console.log('wish sidecar: recall 0..1 + muted excluded + owner-confirmed attribution only');
256
+ }
257
+ {
258
+ const brief = variables['motivation_brief'];
259
+ if (typeof brief !== 'function') throw new Error('FAIL: motivation_brief 变量未注册');
260
+ const saved = await byName('gotry_motivation_save').execute({
261
+ profile: {
262
+ weights: {
263
+ brief_probe: 0.6
264
+ },
265
+ evidence: [
266
+ '用户原话:smoke 读回探针'
267
+ ],
268
+ hard: {
269
+ wake_not_before: '07:15'
270
+ }
271
+ }
272
+ }, null);
273
+ if (saved.ok !== true) throw new Error('FAIL: 读回探针画像应保存成功');
274
+ const rendered = brief();
275
+ if (!rendered.includes('brief_probe=') || !rendered.includes('wake_not_before=07:15') || !/证据 [1-9]\d* 条/.test(rendered)) {
276
+ throw new Error(`FAIL: 回访 brief 应含画像字段,实际:${rendered.slice(0, 200)}`);
277
+ }
278
+ console.log('memory read-back: motivation_brief renders profile for returning sessions (empty = first visit)');
279
+ }
82
280
  console.log('\nSMOKE OK');
83
281
  }
84
282
  main().catch((e)=>{