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

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 (75) hide show
  1. package/README.md +29 -74
  2. package/bin/gotry-inner.js +50 -1
  3. package/bin/gotry-stdio-ask.js +59 -0
  4. package/cordis.gotry-patch.yml +63 -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/flyai.js +131 -0
  12. package/dist/capabilities/hbcli.js +11 -6
  13. package/dist/capabilities/incident-log.js +2 -1
  14. package/dist/capabilities/session/action-cache.js +120 -0
  15. package/dist/capabilities/session/adapters/ctrip-flight.js +83 -0
  16. package/dist/capabilities/session/adapters/meituan-local.js +73 -0
  17. package/dist/capabilities/session/extract.js +40 -0
  18. package/dist/capabilities/session/read-guard.js +56 -0
  19. package/dist/capabilities/session/transport.js +95 -0
  20. package/dist/capabilities/session-search.js +95 -0
  21. package/dist/scripts/action-cache-tests.js +107 -0
  22. package/dist/scripts/agent-reach-tests.js +1 -1
  23. package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
  24. package/dist/scripts/async-collect.js +26 -7
  25. package/dist/scripts/companion-tests.js +112 -0
  26. package/dist/scripts/hbcli-tests.js +18 -2
  27. package/dist/scripts/ledger-tests.js +344 -0
  28. package/dist/scripts/ledger-workflow-crash.js +42 -0
  29. package/dist/scripts/memory-capture-tests.js +77 -0
  30. package/dist/scripts/memory-decay-tests.js +83 -0
  31. package/dist/scripts/memory-metrics.js +24 -0
  32. package/dist/scripts/nudge-digest.js +83 -0
  33. package/dist/scripts/probe-poi-tests.js +9 -0
  34. package/dist/scripts/replay.js +73 -1
  35. package/dist/scripts/session-attach-diagnose.js +31 -0
  36. package/dist/scripts/session-attach-poc.js +65 -0
  37. package/dist/scripts/session-attach-wait.js +41 -0
  38. package/dist/scripts/session-extract-tests.js +81 -0
  39. package/dist/scripts/session-login.js +48 -0
  40. package/dist/scripts/session-tests.js +162 -0
  41. package/dist/scripts/skeleton-check.js +3 -1
  42. package/dist/scripts/skills-contract-tests.js +85 -0
  43. package/dist/scripts/smoke.js +242 -4
  44. package/dist/scripts/state-cli-tests.js +136 -0
  45. package/dist/scripts/state-cli.js +234 -0
  46. package/dist/scripts/time-eval-tests.js +318 -0
  47. package/dist/scripts/travel-timeline-tests.js +123 -0
  48. package/dist/scripts/unified-tests.js +1 -1
  49. package/dist/src/companions.js +112 -0
  50. package/dist/src/dsh-llm.js +27 -7
  51. package/dist/src/index.js +600 -121
  52. package/dist/src/loop.js +89 -25
  53. package/dist/src/memory-capture.js +40 -0
  54. package/dist/src/memory-decay.js +31 -0
  55. package/dist/src/memory-utility.js +55 -0
  56. package/dist/src/mock-llm.js +6 -1
  57. package/dist/src/slot-spec.js +165 -0
  58. package/dist/src/state-ledger.js +848 -0
  59. package/dist/src/time-anchor.js +100 -0
  60. package/dist/src/tool-packet.js +12 -0
  61. package/dist/src/travel-slots.js +144 -0
  62. package/dist/src/travel-timeline.js +78 -0
  63. package/dist/src/unified.js +3 -1
  64. package/dist/src/wish-pool.js +27 -0
  65. package/package.json +6 -2
  66. package/ts/capabilities/hbcli.ts +6 -6
  67. package/ts/capabilities/incident-log.ts +6 -3
  68. package/ts/package.json +3 -0
  69. package/ts/scripts/skeleton-check.ts +5 -1
  70. package/ts/src/dsh-llm.ts +27 -7
  71. package/ts/src/index.ts +439 -91
  72. package/ts/src/loop.ts +109 -34
  73. package/ts/src/mock-llm.ts +13 -1
  74. package/ts/src/unified.ts +6 -1
  75. package/ts/cordis.gotry-patch.yml +0 -36
package/dist/src/loop.js CHANGED
@@ -228,6 +228,26 @@ export async function runTurn(state, userMsg, llm, history = [], solve = null) {
228
228
  } else {
229
229
  const invalid = spec ? validateSpec(spec) : '翻译器未产出 spec';
230
230
  if (spec && !invalid) {
231
+ const ext = await llm.extractSlots([
232
+ ...history,
233
+ {
234
+ role: 'user',
235
+ text: userMsg
236
+ }
237
+ ]);
238
+ if (ext) {
239
+ const { resolveSlots, specDateMismatches } = await import('./slot-spec.js');
240
+ const { buildTimeAnchor } = await import('./time-anchor.js');
241
+ const mismatches = specDateMismatches(spec, resolveSlots(ext, buildTimeAnchor()));
242
+ if (mismatches.length > 0) {
243
+ const lines = mismatches.map((m)=>`- ${m.specAt}:翻译说 ${m.specDate},你的原话换算是 ${m.slotDate}(${m.field})`);
244
+ parts.push(`⚠️ 日期分歧,确认后我再算(以你为准):\n${lines.join('\n')}`);
245
+ return {
246
+ reply: parts.join('\n\n'),
247
+ state
248
+ };
249
+ }
250
+ }
231
251
  state.spec = spec;
232
252
  state.solve = await solve(spec);
233
253
  parts.push(llm.render ? await llm.render(state) : renderSolve(state));
@@ -244,50 +264,94 @@ export async function runTurn(state, userMsg, llm, history = [], solve = null) {
244
264
  };
245
265
  }
246
266
  import { join } from 'node:path';
247
- import { mkdir } from 'node:fs/promises';
248
- import { readFile, writeFile } from 'node:fs/promises';
249
- const ASYNC_DIR = 'gotry-state/async';
250
- async function asyncPath(name) {
251
- await mkdir(ASYNC_DIR, {
267
+ import { mkdir, rename, writeFile, readFile } from 'node:fs/promises';
268
+ import { ensureLedger, openLedgerIfExists } from './state-ledger.js';
269
+ async function asyncDir(stateRoot) {
270
+ const root = stateRoot === '.' ? process.cwd() : stateRoot;
271
+ const dir = join(root, 'gotry-state', 'async');
272
+ await mkdir(dir, {
252
273
  recursive: true
253
274
  });
254
- return join(ASYNC_DIR, name);
275
+ return dir;
276
+ }
277
+ async function atomicWrite(path, text) {
278
+ const tmp = `${path}.tmp`;
279
+ await writeFile(tmp, text, 'utf-8');
280
+ await rename(tmp, path);
255
281
  }
256
- export async function persistAsyncTicket(ticket, state) {
257
- const p = await asyncPath(`${ticket.id}.json`);
258
- await writeFile(p, JSON.stringify({
282
+ export async function persistAsyncTicket(ticket, state, stateRoot = '.') {
283
+ const ledger = ensureLedger(stateRoot);
284
+ ledger.createWorkflowRun({
285
+ id: ticket.id,
286
+ goal: ticket.objective,
287
+ ticket,
288
+ state
289
+ });
290
+ const dir = await asyncDir(stateRoot);
291
+ const p = join(dir, `${ticket.id}.json`);
292
+ await atomicWrite(p, JSON.stringify({
259
293
  ticket,
260
294
  state
261
- }, null, 2), 'utf-8');
295
+ }, null, 2));
262
296
  return p;
263
297
  }
264
- export async function loadAsyncTicket(ticketId) {
298
+ export async function loadAsyncTicket(ticketId, stateRoot = '.') {
299
+ const ledger = openLedgerIfExists(stateRoot);
300
+ const run = ledger?.getWorkflowRun(ticketId);
301
+ if (run) {
302
+ return {
303
+ ticket: JSON.parse(run.ticket_json),
304
+ state: JSON.parse(run.state_json)
305
+ };
306
+ }
265
307
  try {
266
- const p = await asyncPath(`${ticketId}.json`);
308
+ const p = join(await asyncDir(stateRoot), `${ticketId}.json`);
267
309
  return JSON.parse(await readFile(p, 'utf-8'));
268
310
  } catch {
269
311
  return null;
270
312
  }
271
313
  }
272
- export async function settleAsyncTicket(ticketId, reply) {
273
- const p = await asyncPath(`${ticketId}.deliverable.md`);
274
- await writeFile(p, reply, 'utf-8');
314
+ export async function settleAsyncTicket(ticketId, reply, stateRoot = '.') {
315
+ openLedgerIfExists(stateRoot)?.settleWorkflowRun(ticketId, reply);
316
+ const dir = await asyncDir(stateRoot);
317
+ const p = join(dir, `${ticketId}.deliverable.md`);
318
+ await atomicWrite(p, reply);
275
319
  return p;
276
320
  }
321
+ export function makeJournaledSolvePort(ledger, runId, solve, opts) {
322
+ return async (spec)=>{
323
+ const step = ledger.getWorkflowStep(runId, 'solve');
324
+ if (step?.status === 'done' && step.result) {
325
+ return JSON.parse(step.result);
326
+ }
327
+ ledger.markStepIntent(runId, 'solve');
328
+ opts?.onRealSolve?.();
329
+ const r = await solve(spec);
330
+ ledger.markStepDone(runId, 'solve', r);
331
+ return r;
332
+ };
333
+ }
277
334
  export function probePoi(msg) {
278
335
  const trimmed = msg.trim();
279
336
  if (!trimmed) return null;
280
- const isShort = trimmed.length <= 24 && !/[??!!。.,,]/.test(trimmed);
281
- if (isShort) {
282
- const stripped = trimmed.replace(/^(查一下|查|搜|找|看看|推荐|告诉我|检索)\s*/, '').trim();
283
- if (stripped.length >= 2) return stripped.slice(0, 24);
284
- }
337
+ const cutAtPunct = (s)=>s.split(/[,,。.;;!!??]/)[0]?.trim() ?? '';
285
338
  const m = trimmed.match(/(查一下|查|搜|找|看看|推荐|告诉我|检索)\s*(.{2,24})/);
286
- if (m) return m[2].trim().slice(0, 24);
287
- if (/(酒店|民宿|客栈|饭店|有什么|玩什么|有哪些)/.test(trimmed)) {
288
- const place = trimmed.match(/([一-龥a-zA-Z]{2,12})\s*(?:酒店|民宿|客栈|饭店|有什么|玩什么|有哪些)/);
289
- if (place) return place[1];
290
- return trimmed.replace(/(酒店|民宿|客栈|饭店|有什么|玩什么|有哪些)/, '').trim().slice(0, 24) || trimmed.slice(0, 24);
339
+ if (m?.[2]) {
340
+ const obj = cutAtPunct(m[2]);
341
+ if (obj.length >= 2) return obj.slice(0, 24);
342
+ }
343
+ if (/(酒店|民宿|客栈|饭店)/.test(trimmed)) {
344
+ const parts = trimmed.split(/(酒店|民宿|客栈|饭店)/);
345
+ const after = cutAtPunct((parts[2] ?? '').replace(/^[的::\s]+/, ''));
346
+ if (after.length >= 2 && /[A-Za-z0-9]/.test(after)) return after.slice(0, 24);
347
+ const nounAt = trimmed.search(/(酒店|民宿|客栈|饭店)/);
348
+ const pre = trimmed.slice(Math.max(0, nounAt - 6), nounAt).match(/[一-龥]{2,6}$/);
349
+ if (pre && !/(机票|车票|和|做|订|的)$/.test(pre[0])) return pre[0];
350
+ }
351
+ const q = trimmed.match(/([一-龥a-zA-Z]{2,12})\s*(?:有什么|玩什么|有哪些)/);
352
+ if (q?.[1]) return q[1];
353
+ if (trimmed.length <= 12 && !/[,,。.;;!!??::]/.test(trimmed) && !/[是有在要想得到说订吧呢]/.test(trimmed)) {
354
+ return trimmed.slice(0, 24);
291
355
  }
292
356
  return null;
293
357
  }
@@ -0,0 +1,40 @@
1
+ export function mergeProfile(current, patch) {
2
+ if (!patch) return null;
3
+ const base = {
4
+ weights: current?.weights ?? {},
5
+ evidence: current?.evidence ?? [],
6
+ hard: current?.hard ?? {}
7
+ };
8
+ const newEvidence = (patch.evidence ?? []).filter(Boolean).filter((e)=>!base.evidence.includes(e));
9
+ const patchWeights = patch.weights;
10
+ const weightsChanged = !!patchWeights && Object.keys(patchWeights).length > 0 && JSON.stringify(patchWeights) !== JSON.stringify(base.weights);
11
+ const hardChanged = !!patch.hard && Object.keys(patch.hard).length > 0 && JSON.stringify(patch.hard) !== JSON.stringify(base.hard);
12
+ if (newEvidence.length === 0 && !weightsChanged && !hardChanged) return null;
13
+ let weights = {
14
+ ...base.weights
15
+ };
16
+ if (weightsChanged && newEvidence.length > 0 && patchWeights) {
17
+ weights = {
18
+ ...weights,
19
+ ...patchWeights
20
+ };
21
+ const sum = Object.values(weights).reduce((a, b)=>a + b, 0);
22
+ if (sum > 0) for (const k of Object.keys(weights))weights[k] = Math.round(weights[k] / sum * 100) / 100;
23
+ }
24
+ return {
25
+ weights,
26
+ evidence: [
27
+ ...base.evidence,
28
+ ...newEvidence
29
+ ],
30
+ hard: hardChanged ? {
31
+ ...base.hard,
32
+ ...patch.hard ?? {}
33
+ } : {
34
+ ...base.hard
35
+ }
36
+ };
37
+ }
38
+
39
+
40
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/memory-capture.ts
@@ -0,0 +1,31 @@
1
+ export function windowFactor(ageDays) {
2
+ if (ageDays <= 30) return 1.0;
3
+ if (ageDays <= 90) return 0.75;
4
+ if (ageDays <= 180) return 0.5;
5
+ if (ageDays <= 365) return 0.25;
6
+ return 0.1;
7
+ }
8
+ export const KIND_WEIGHT = {
9
+ recalled: 0.25,
10
+ applied: 0.5,
11
+ verified_outcome: 1.0
12
+ };
13
+ export function ageDaysOf(ts, now) {
14
+ const then = new Date(ts).getTime();
15
+ if (Number.isNaN(then)) return Number.POSITIVE_INFINITY;
16
+ return Math.max(0, (now.getTime() - then) / 86_400_000);
17
+ }
18
+ export function eventDecayScore(ev, now) {
19
+ const w = KIND_WEIGHT[ev.kind] ?? 0;
20
+ return round4(w * windowFactor(ageDaysOf(ev.ts, now)));
21
+ }
22
+ export function decayedConfidence(events, now) {
23
+ const sum = events.reduce((acc, ev)=>acc + eventDecayScore(ev, now), 0);
24
+ return round4(Math.min(1, sum));
25
+ }
26
+ function round4(x) {
27
+ return Math.round(x * 10_000) / 10_000;
28
+ }
29
+
30
+
31
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/memory-decay.ts
@@ -0,0 +1,55 @@
1
+ export function makeEventId(ev) {
2
+ return [
3
+ ev.wish_id,
4
+ ev.kind,
5
+ ev.ctx ?? '',
6
+ ev.attribution ?? ''
7
+ ].join('|');
8
+ }
9
+ export function appendEvent(events, ev) {
10
+ const full = {
11
+ schema: 'memory_utility_observation.v0',
12
+ event_id: makeEventId(ev),
13
+ ...ev
14
+ };
15
+ if (events.some((e)=>e.event_id === full.event_id)) return {
16
+ events,
17
+ appended: false
18
+ };
19
+ if (ev.kind === 'verified_outcome' && !ev.attribution) {
20
+ return appendEvent(events, {
21
+ ...ev,
22
+ kind: 'applied'
23
+ });
24
+ }
25
+ return {
26
+ events: [
27
+ ...events,
28
+ full
29
+ ],
30
+ appended: true
31
+ };
32
+ }
33
+ export function projectUtility(events) {
34
+ const out = {};
35
+ for (const ev of events){
36
+ const w = out[ev.wish_id] ??= {
37
+ wish_id: ev.wish_id,
38
+ status: 'unknown',
39
+ recalled: 0,
40
+ applied: 0,
41
+ verified: 0
42
+ };
43
+ if (ev.kind === 'recalled') w.recalled++;
44
+ if (ev.kind === 'applied') w.applied++;
45
+ if (ev.kind === 'verified_outcome') {
46
+ w.verified++;
47
+ if (ev.attribution) w.status = ev.attribution;
48
+ }
49
+ w.last_event_ts = ev.ts;
50
+ }
51
+ return out;
52
+ }
53
+
54
+
55
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/memory-utility.ts
@@ -50,7 +50,7 @@ const SCRIPT = [
50
50
  }
51
51
  }
52
52
  ];
53
- export function createMockLlm(flightPackPath) {
53
+ export function createMockLlm(flightPackPath, slotScript = []) {
54
54
  let stepIdx = 0;
55
55
  return {
56
56
  async extractFacts (history, _state) {
@@ -88,6 +88,11 @@ export function createMockLlm(flightPackPath) {
88
88
  spec.budgetCny = 9000;
89
89
  return spec;
90
90
  },
91
+ async extractSlots (history) {
92
+ const last = history[history.length - 1]?.text ?? '';
93
+ const hit = slotScript.find((s)=>last.includes(s.when));
94
+ return hit ? structuredClone(hit.extraction) : null;
95
+ },
91
96
  async polishQuestion (q) {
92
97
  return `【${q.key}】${q.text}\n(为什么问:${q.why})`;
93
98
  }
@@ -0,0 +1,165 @@
1
+ import { parseAbsoluteDate, ymd } from './time-anchor.js';
2
+ const WEEKDAY_ZH = {
3
+ 日: 0,
4
+ 一: 1,
5
+ 二: 2,
6
+ 三: 3,
7
+ 四: 4,
8
+ 五: 5,
9
+ 六: 6
10
+ };
11
+ function addDaysYmd(today, n) {
12
+ const [y, m, d] = today.split('-').map(Number);
13
+ return ymd(new Date(y, m - 1, d + n));
14
+ }
15
+ function splitPlusSuffix(expr) {
16
+ const m = expr.match(/^(.*?)\+(\d+)天?$/);
17
+ if (m && m[1]) return {
18
+ base: m[1],
19
+ plusDays: Number(m[2])
20
+ };
21
+ return {
22
+ base: expr,
23
+ plusDays: 0
24
+ };
25
+ }
26
+ function neighborDayOffset(expr) {
27
+ if (expr === '今天') return 0;
28
+ if (expr === '明天') return 1;
29
+ if (expr === '后天') return 2;
30
+ if (expr === '大后天') return 3;
31
+ return null;
32
+ }
33
+ function weekdayOffsetDays(expr, today) {
34
+ const m = expr.match(/^(本|下|下下)周([日一二三四五六])$/);
35
+ if (!m || !m[2]) return null;
36
+ const target = WEEKDAY_ZH[m[2]];
37
+ if (target === undefined) return null;
38
+ const [y, mm, d] = today.split('-').map(Number);
39
+ const todayMonOffset = (new Date(y, mm - 1, d).getDay() + 6) % 7;
40
+ const targetMonOffset = (target + 6) % 7;
41
+ const weekShift = m[1] === '本' ? 0 : m[1] === '下' ? 7 : 14;
42
+ return weekShift + targetMonOffset - todayMonOffset;
43
+ }
44
+ function monthFragmentDate(expr, today) {
45
+ if (![
46
+ '下个月初',
47
+ '下个月中旬',
48
+ '下个月下旬'
49
+ ].includes(expr)) return null;
50
+ const [y, m] = today.split('-').map(Number);
51
+ const day = expr === '下个月初' ? 1 : expr === '下个月中旬' ? 11 : 21;
52
+ return ymd(new Date(y, m, day));
53
+ }
54
+ export function resolveSlotDate(expr, anchor) {
55
+ const raw = expr.trim();
56
+ const year = Number(anchor.today.slice(0, 4));
57
+ const absolute = parseAbsoluteDate(raw, year);
58
+ if (absolute) return {
59
+ raw,
60
+ date: absolute,
61
+ kind: 'absolute'
62
+ };
63
+ const { base, plusDays } = splitPlusSuffix(raw);
64
+ const baseAbs = parseAbsoluteDate(base, year);
65
+ if (baseAbs) return {
66
+ raw,
67
+ date: addDaysYmd(baseAbs, plusDays),
68
+ kind: 'absolute'
69
+ };
70
+ const month = monthFragmentDate(base, anchor.today);
71
+ if (month) return {
72
+ raw,
73
+ date: addDaysYmd(month, plusDays),
74
+ kind: 'card'
75
+ };
76
+ const neighbor = neighborDayOffset(base);
77
+ if (neighbor !== null) return {
78
+ raw,
79
+ date: addDaysYmd(anchor.today, neighbor + plusDays),
80
+ kind: 'card'
81
+ };
82
+ const offset = weekdayOffsetDays(base, anchor.today);
83
+ if (offset !== null) return {
84
+ raw,
85
+ date: addDaysYmd(anchor.today, offset + plusDays),
86
+ kind: 'card'
87
+ };
88
+ return {
89
+ raw,
90
+ date: null,
91
+ kind: 'unresolved'
92
+ };
93
+ }
94
+ const DATE_FIELDS = {
95
+ requisition: [
96
+ 'start_date',
97
+ 'end_date'
98
+ ],
99
+ flight: [
100
+ 'departure_date',
101
+ 'return_date'
102
+ ],
103
+ hotel: [
104
+ 'check_in_date',
105
+ 'check_out_date'
106
+ ]
107
+ };
108
+ export function resolveSlots(ext, anchor) {
109
+ const out = {
110
+ unresolved: []
111
+ };
112
+ for (const domain of ext.domains){
113
+ const slots = ext.slots[domain];
114
+ if (!slots) continue;
115
+ for (const field of DATE_FIELDS[domain] ?? []){
116
+ const value = slots[field];
117
+ if (typeof value !== 'string') continue;
118
+ const res = resolveSlotDate(value, anchor);
119
+ const bucket = out[domain] ??= {};
120
+ bucket[field] = res;
121
+ if (res.kind === 'unresolved') out.unresolved.push({
122
+ field: `${domain}.${field}`,
123
+ raw: res.raw
124
+ });
125
+ }
126
+ }
127
+ return out;
128
+ }
129
+ export function specDateMismatches(spec, resolved) {
130
+ const mismatches = [];
131
+ const primary = [
132
+ {
133
+ field: 'requisition.start_date',
134
+ res: resolved.requisition?.start_date
135
+ },
136
+ {
137
+ field: 'flight.departure_date',
138
+ res: resolved.flight?.departure_date
139
+ },
140
+ {
141
+ field: 'hotel.check_in_date',
142
+ res: resolved.hotel?.check_in_date
143
+ }
144
+ ];
145
+ const dated = primary.filter((p)=>p.res?.date != null);
146
+ if (dated.length === 0) return mismatches;
147
+ const datedSegs = (spec.segments ?? []).filter((s)=>typeof s.date === 'string' && s.date);
148
+ if (datedSegs.length !== 1) return mismatches;
149
+ const seg = datedSegs[0];
150
+ const slotDates = new Set(dated.map((s)=>s.res.date));
151
+ if (!slotDates.has(seg.date)) {
152
+ mismatches.push({
153
+ specAt: `segments[${(spec.segments ?? []).indexOf(seg)}].date`,
154
+ field: dated.map((s)=>s.field).join('|'),
155
+ specDate: seg.date,
156
+ slotDate: [
157
+ ...slotDates
158
+ ].join('|')
159
+ });
160
+ }
161
+ return mismatches;
162
+ }
163
+
164
+
165
+ //# sourceURL=/Users/bytedance/work/gotry/ts/src/slot-spec.ts