@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.
- package/README.md +27 -74
- package/bin/gotry-inner.js +50 -1
- package/bin/gotry-stdio-ask.js +59 -0
- package/cordis.gotry-patch.yml +56 -5
- package/data/flights_2026.json +7 -4
- package/data/hotels_2026.json +102 -15
- package/data/time-slot-eval.json +384 -0
- package/data/yunnan-pack.json +2 -1
- package/dist/capabilities/agent-reach-bridge.py +90 -0
- package/dist/capabilities/anything.js +2 -2
- package/dist/capabilities/hbcli.js +11 -6
- package/dist/capabilities/incident-log.js +2 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/hbcli-tests.js +18 -2
- package/dist/scripts/memory-capture-tests.js +77 -0
- package/dist/scripts/memory-metrics.js +38 -0
- package/dist/scripts/nudge-digest.js +93 -0
- package/dist/scripts/probe-poi-tests.js +9 -0
- package/dist/scripts/replay.js +73 -1
- package/dist/scripts/skeleton-check.js +3 -1
- package/dist/scripts/skills-contract-tests.js +85 -0
- package/dist/scripts/smoke.js +201 -3
- package/dist/scripts/time-eval-tests.js +318 -0
- package/dist/src/dsh-llm.js +27 -7
- package/dist/src/index.js +342 -92
- package/dist/src/loop.js +37 -10
- package/dist/src/memory-capture.js +40 -0
- package/dist/src/memory-utility.js +55 -0
- package/dist/src/mock-llm.js +6 -1
- package/dist/src/slot-spec.js +165 -0
- package/dist/src/time-anchor.js +100 -0
- package/dist/src/tool-packet.js +12 -0
- package/dist/src/travel-slots.js +144 -0
- package/dist/src/wish-pool.js +27 -0
- package/package.json +6 -2
- package/ts/capabilities/hbcli.ts +6 -6
- package/ts/capabilities/incident-log.ts +6 -3
- package/ts/scripts/skeleton-check.ts +5 -1
- package/ts/src/dsh-llm.ts +27 -7
- package/ts/src/index.ts +292 -77
- package/ts/src/loop.ts +52 -17
- package/ts/src/mock-llm.ts +13 -1
- package/ts/cordis.gotry-patch.yml +0 -36
package/ts/capabilities/hbcli.ts
CHANGED
|
@@ -120,14 +120,14 @@ export async function searchHotels(
|
|
|
120
120
|
query: { destination: string; checkIn?: string; checkOut?: string; adults?: number },
|
|
121
121
|
opts: HbcliCallOptions & { fallbackPath?: string } = {},
|
|
122
122
|
): Promise<HbcliCallResult & { hotels?: unknown; summary: string }> {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (query.
|
|
127
|
-
if (query.adults) hbArgs.push('--
|
|
123
|
+
// 旗标对齐上游 CLI v0.3.0(命令树扁平化后):--destination-name + --room-occupancies;
|
|
124
|
+
// hotel-list 无日期旗标(房价按上游当前窗口),checkIn/checkOut 留给 hotel-rates 跟进
|
|
125
|
+
const hbArgs = ['search', 'hotel-list', '--json', '--page-size', '10']
|
|
126
|
+
if (query.destination) hbArgs.push('--destination-name', query.destination)
|
|
127
|
+
if (query.adults) hbArgs.push('--room-occupancies', JSON.stringify([{ adultCount: query.adults, childrenAges: [] }]))
|
|
128
128
|
const live = await callHbcliJson(hbArgs, opts)
|
|
129
129
|
if (live.via === 'hbcli-realtime') {
|
|
130
|
-
return { ...live, hotels: live.result, summary: `${query.destination}:hbcli
|
|
130
|
+
return { ...live, hotels: live.result, summary: `${query.destination}:hbcli 实时返回${query.checkIn || query.checkOut ? '(日期不传上游 list,以当前窗口房价返回)' : ''}` }
|
|
131
131
|
}
|
|
132
132
|
// 降级:读静态包
|
|
133
133
|
const fallback = opts.fallbackPath
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
16
16
|
import { dirname, isAbsolute, join } from 'node:path'
|
|
17
17
|
import { fileURLToPath } from 'node:url'
|
|
18
|
+
import type { ToolFailure } from '../src/tool-packet.ts'
|
|
18
19
|
|
|
19
20
|
export type IncidentKind = 'uncaughtException' | 'unhandledRejection' | 'plugin_error' | 'tool_execute_error'
|
|
20
21
|
|
|
@@ -74,7 +75,7 @@ export function installProcessGuards(stateRoot: string, labels?: { uncaughtExcep
|
|
|
74
75
|
installed = true
|
|
75
76
|
logPath = resolveIncidentsPath(stateRoot)
|
|
76
77
|
|
|
77
|
-
const uncaughtHandler = (err: Error, origin: NodeJS.
|
|
78
|
+
const uncaughtHandler = (err: Error, origin: NodeJS.UncaughtExceptionOrigin | 'uncaughtException') => {
|
|
78
79
|
recordIncident({
|
|
79
80
|
ts: new Date().toISOString(),
|
|
80
81
|
kind: 'uncaughtException',
|
|
@@ -111,6 +112,7 @@ export function installProcessGuards(stateRoot: string, labels?: { uncaughtExcep
|
|
|
111
112
|
* 工具执行面异常隔离(D-NEW gotry 侧收尾):dsh 的一个工具 execute 抛错/拒绝
|
|
112
113
|
* 会沿 cordis 传到主循环,拖垮整个会话。包装后:降级为结构化错误返回给 LLM、
|
|
113
114
|
* 事故落盘,永不向上抛。落盘失败也不抛(双保险,仍返回结构化错误)。
|
|
115
|
+
* 失败形状 = ToolFailure(RFC S1 observation envelope 的失败分支,编译期对齐)。
|
|
114
116
|
*/
|
|
115
117
|
export function guardToolExecute<A, R>(name: string, stateRoot: string, execute: (args: A, exec: unknown) => R | Promise<R>): (args: A, exec: unknown) => Promise<R> {
|
|
116
118
|
return async (args: A, exec: unknown): Promise<R> => {
|
|
@@ -125,11 +127,12 @@ export function guardToolExecute<A, R>(name: string, stateRoot: string, execute:
|
|
|
125
127
|
stack: (err.stack ?? '').slice(0, 4000),
|
|
126
128
|
source: name,
|
|
127
129
|
}, stateRoot)
|
|
128
|
-
|
|
130
|
+
const failure: ToolFailure = {
|
|
129
131
|
ok: false,
|
|
130
132
|
summary: `gotry_${name} 内部错误(已隔离,会话继续): ${err.message.slice(0, 300)}`,
|
|
131
133
|
evidence: `[incident:tool_execute_error@${new Date().toISOString()}]`,
|
|
132
|
-
}
|
|
134
|
+
}
|
|
135
|
+
return failure as R
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
138
|
}
|
|
@@ -40,6 +40,10 @@ export async function checkConnectivity(a: string, b: string): Promise<{ connect
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// CLI 直跑
|
|
43
|
-
|
|
43
|
+
// 仅作为直接 CLI 入口时执行(被 import 时不触发:此前 dsh 子进程的
|
|
44
|
+
// argv[2]/[3](--profile headless)恰好真值,每次启动白跑一次探测还污染 stdout)
|
|
45
|
+
import { pathToFileURL } from 'node:url'
|
|
46
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
47
|
+
if (isMain && process.argv[2] && process.argv[3]) {
|
|
44
48
|
console.log((await checkConnectivity(process.argv[2], process.argv[3])).evidence)
|
|
45
49
|
}
|
package/ts/src/dsh-llm.ts
CHANGED
|
@@ -10,18 +10,22 @@
|
|
|
10
10
|
import type { LlmPort } from './loop.ts'
|
|
11
11
|
import type { InterviewQuestion, TravelerProfile, TripState, Turn, CalendarState } from './contracts.ts'
|
|
12
12
|
import { parseFlightPackToSpec, type JourneySpecTS } from './unified.ts'
|
|
13
|
+
import { buildTimeAnchor } from './time-anchor.ts'
|
|
14
|
+
import { buildSlotSystem, flagExpiredSlots, normalizeExtraction, type TravelSlotExtraction } from './travel-slots.ts'
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
// 惰性读取(env 在调用时取值):模块顶常量会在 .env 加载前冻结(ESM import 提升),
|
|
17
|
+
// 脚本先 loadEnv 再 import 也救不了——401 错配(key 发往默认端点)的存量隐患由此根除。
|
|
18
|
+
const model = () => process.env['LLM_MODEL'] ?? process.env['DEEPSEEK_MODEL'] ?? 'MiniMax-M2'
|
|
19
|
+
const base = () => (process.env['LLM_BASE_URL'] ?? process.env['DEEPSEEK_BASE_URL'] ?? 'https://api.minimax.io/v1').replace(/\/$/, '')
|
|
16
20
|
|
|
17
21
|
async function chat(messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>, json: boolean): Promise<string> {
|
|
18
22
|
const key = process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY']
|
|
19
23
|
if (!key) throw new Error('LLM_API_KEY 未设置(兼容 DEEPSEEK_API_KEY 别名)——真 LLM 路径不可用,请回退 mock(ADR-8)')
|
|
20
|
-
const res = await fetch(`${
|
|
24
|
+
const res = await fetch(`${base()}/chat/completions`, {
|
|
21
25
|
method: 'POST',
|
|
22
26
|
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}` },
|
|
23
27
|
body: JSON.stringify({
|
|
24
|
-
model:
|
|
28
|
+
model: model(),
|
|
25
29
|
messages,
|
|
26
30
|
...(json ? { response_format: { type: 'json_object' } } : {}),
|
|
27
31
|
temperature: json ? 0 : 0.7,
|
|
@@ -58,13 +62,16 @@ const SKELETON_SYSTEM = `你是行程骨架抽取器。从对话中抽取行程
|
|
|
58
62
|
规则:每个跨城移动一段;锚点只放用户明说或必然的(如"当天到"→arriveByMin 23:59=1439);时刻用当日分钟。
|
|
59
63
|
scenario 判定:「洱海/大理/千岛湖/太湖+选目的地」→erhai(候选集);「普吉/workation/远程办公+多城链」→workation(五段链);「云南/大理丽江」→yunnan;不确定→generic。只输出 JSON。`
|
|
60
64
|
|
|
61
|
-
export function createOpenAICompatLlm(flightPackPath?: string): LlmPort {
|
|
65
|
+
export function createOpenAICompatLlm(flightPackPath?: string, clock: () => Date = () => new Date()): LlmPort {
|
|
62
66
|
const pack = flightPackPath
|
|
63
67
|
const historyText = (h: Turn[]) => h.map(t => `${t.role === 'user' ? '用户' : '助手'}: ${t.text}`).join('\n')
|
|
68
|
+
// 时间锚点卡:每条抽取链路都带上「今天」——legacy 路径此前无时间注入,
|
|
69
|
+
// 过期/相对日期语义全靠它(算术在 time-anchor 层,LLM 只查卡)。
|
|
70
|
+
const anchorContext = () => `时间锚点卡:\n${buildTimeAnchor(clock()).card}`
|
|
64
71
|
return {
|
|
65
72
|
async extractFacts(history) {
|
|
66
73
|
const out = await chat(
|
|
67
|
-
[{ role: 'system', content: FACTS_SYSTEM }, { role: 'user', content: historyText(history) }],
|
|
74
|
+
[{ role: 'system', content: FACTS_SYSTEM }, { role: 'user', content: `${anchorContext()}\n\n${historyText(history)}` }],
|
|
68
75
|
true,
|
|
69
76
|
)
|
|
70
77
|
const obj = parseJsonBlock(out)
|
|
@@ -76,7 +83,7 @@ export function createOpenAICompatLlm(flightPackPath?: string): LlmPort {
|
|
|
76
83
|
},
|
|
77
84
|
async extractSpec(history, state) {
|
|
78
85
|
// 架构(ADR-10):LLM 只产骨架与锚点;班次数据永远来自能力层(数据包/未来实时API)
|
|
79
|
-
const context =
|
|
86
|
+
const context = `${anchorContext()}\n已断言日历:${JSON.stringify(state.calendar.assertedWeekdays)}\nprofile:${JSON.stringify(state.profile)}`
|
|
80
87
|
const out = await chat(
|
|
81
88
|
[{ role: 'system', content: SKELETON_SYSTEM }, { role: 'user', content: `${context}\n\n${historyText(history)}` }],
|
|
82
89
|
true,
|
|
@@ -123,6 +130,19 @@ export function createOpenAICompatLlm(flightPackPath?: string): LlmPort {
|
|
|
123
130
|
packSpec.budgetCny = 9000
|
|
124
131
|
return packSpec
|
|
125
132
|
},
|
|
133
|
+
async extractSlots(history, now?: Date): Promise<TravelSlotExtraction | null> {
|
|
134
|
+
// now 可注入(评测固定锚点);过期判定在 flagExpiredSlots(代码层),模型只管逐字抽取
|
|
135
|
+
const anchor = buildTimeAnchor(now ?? clock())
|
|
136
|
+
const out = await chat(
|
|
137
|
+
[{ role: 'system', content: buildSlotSystem(anchor) }, { role: 'user', content: historyText(history) }],
|
|
138
|
+
true,
|
|
139
|
+
)
|
|
140
|
+
const obj = parseJsonBlock(out)
|
|
141
|
+
if (!obj) return null
|
|
142
|
+
// language 判定归代码层(detectLanguage),模型输出仅供参考;过期判定同在代码层
|
|
143
|
+
const ext = normalizeExtraction(obj, history.map(t => t.text).join('\n'))
|
|
144
|
+
return ext ? flagExpiredSlots(ext, anchor) : null
|
|
145
|
+
},
|
|
126
146
|
async polishQuestion(q: InterviewQuestion) {
|
|
127
147
|
const out = await chat(
|
|
128
148
|
[{ role: 'system', content: '把旅行规划的追问润色得更自然,保留全部信息(含为什么问),一两句话,不要加表情。' },
|