@danceiny/gotry 0.0.1-rc.13 → 0.0.1-rc.15
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 +121 -140
- package/README.zh-CN.md +235 -0
- package/bin/gotry-bootstrap.js +165 -0
- package/bin/gotry-inner.js +10 -3
- package/dist/capabilities/artifacts.js +217 -0
- package/dist/capabilities/hbcli.js +42 -15
- package/dist/scripts/booking-saga-tests.js +187 -0
- package/dist/scripts/bootstrap-tests.js +66 -0
- package/dist/scripts/flyai-tests.js +95 -0
- package/dist/scripts/hbcli-tests.js +28 -3
- package/dist/scripts/nightly-evidence-tests.js +123 -0
- package/dist/scripts/nightly-evidence.js +233 -0
- package/dist/scripts/smoke.js +83 -0
- package/dist/src/booking-saga.js +153 -0
- package/dist/src/dsh-llm.js +30 -6
- package/dist/src/index.js +141 -3
- package/package.json +5 -2
- package/ts/capabilities/artifacts.ts +235 -0
- package/ts/capabilities/hbcli.ts +51 -19
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/dsh-llm.ts +40 -3
- package/ts/src/index.ts +145 -25
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { createMockLlm } from '../src/mock-llm.js';
|
|
5
|
+
import { createOpenAICompatLlm } from '../src/dsh-llm.js';
|
|
6
|
+
import { newState, runTurn } from '../src/loop.js';
|
|
7
|
+
import { solveUnified } from '../src/unified.js';
|
|
8
|
+
import { realtimeSolvePort } from '../src/realtime-pricing.js';
|
|
9
|
+
import { parseNightlyRun } from './product-metrics.js';
|
|
10
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
11
|
+
const DEFAULT_BUDGET_USD = 1;
|
|
12
|
+
function canonical(value) {
|
|
13
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`;
|
|
14
|
+
if (typeof value === 'object' && value !== null) {
|
|
15
|
+
const record = value;
|
|
16
|
+
return `{${Object.keys(record).sort().map((key)=>`${JSON.stringify(key)}:${canonical(record[key])}`).join(',')}}`;
|
|
17
|
+
}
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
function sha256Of(value) {
|
|
21
|
+
return createHash('sha256').update(canonical(value)).digest('hex');
|
|
22
|
+
}
|
|
23
|
+
function round6(value) {
|
|
24
|
+
return Number(value.toFixed(6));
|
|
25
|
+
}
|
|
26
|
+
export function loadPromptSet(path) {
|
|
27
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
28
|
+
if (raw['schema_version'] !== 'gotry_m3_nightly_prompt_set_v1') {
|
|
29
|
+
throw new Error(`prompt set schema_version must be gotry_m3_nightly_prompt_set_v1, got: ${String(raw['schema_version'])}`);
|
|
30
|
+
}
|
|
31
|
+
const turns = raw['turns'];
|
|
32
|
+
if (!Array.isArray(turns) || turns.length === 0 || turns.some((t)=>typeof t !== 'string' || t.length === 0)) {
|
|
33
|
+
throw new Error('prompt set turns must be a non-empty array of non-empty strings');
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
schema_version: 'gotry_m3_nightly_prompt_set_v1',
|
|
37
|
+
description: raw['description'],
|
|
38
|
+
turns: turns
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function assertPriceEntry(label, value) {
|
|
42
|
+
const entry = typeof value === 'object' && value !== null ? value : null;
|
|
43
|
+
if (!entry) throw new Error(`${label} must be an object`);
|
|
44
|
+
for (const field of [
|
|
45
|
+
'input_cache_miss_usd_per_1m_peak',
|
|
46
|
+
'input_cache_hit_usd_per_1m_peak',
|
|
47
|
+
'output_usd_per_1m_peak'
|
|
48
|
+
]){
|
|
49
|
+
const num = entry[field];
|
|
50
|
+
if (typeof num !== 'number' || !Number.isFinite(num) || num < 0) {
|
|
51
|
+
throw new Error(`${label}.${field} must be a non-negative finite number`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
export function loadPriceTable(path) {
|
|
57
|
+
const raw = JSON.parse(readFileSync(path, 'utf8'));
|
|
58
|
+
if (raw['schema_version'] !== 'gotry_llm_price_table_v1') {
|
|
59
|
+
throw new Error(`price table schema_version must be gotry_llm_price_table_v1, got: ${String(raw['schema_version'])}`);
|
|
60
|
+
}
|
|
61
|
+
const aliases = typeof raw['aliases'] === 'object' && raw['aliases'] !== null ? raw['aliases'] : {};
|
|
62
|
+
for (const [from, to] of Object.entries(aliases)){
|
|
63
|
+
if (typeof to !== 'string') throw new Error(`price table alias ${from} must map to a model name`);
|
|
64
|
+
}
|
|
65
|
+
const prices = {};
|
|
66
|
+
for (const [name, entry] of Object.entries(typeof raw['prices'] === 'object' && raw['prices'] !== null ? raw['prices'] : {})){
|
|
67
|
+
prices[name] = assertPriceEntry(`price table entry ${name}`, entry);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
schema_version: 'gotry_llm_price_table_v1',
|
|
71
|
+
aliases,
|
|
72
|
+
prices
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function priceRunCost(model, usage, table) {
|
|
76
|
+
if (usage.responsesMissingUsage > 0) {
|
|
77
|
+
throw new Error(`usage provider omitted usage on ${usage.responsesMissingUsage} response(s) — cost unprovable, refusing to price (fail-closed)`);
|
|
78
|
+
}
|
|
79
|
+
const alias = table.aliases[model];
|
|
80
|
+
if (alias && !table.prices[alias]) throw new Error(`price table alias ${model} -> ${alias} has no entry; update data/llm-price-table.json via PR`);
|
|
81
|
+
const entry = table.prices[alias ?? model];
|
|
82
|
+
if (!entry) throw new Error(`price table has no entry for model ${model}; update data/llm-price-table.json via PR (fail-closed, no guessed price)`);
|
|
83
|
+
const cost = (usage.inputCacheMissTokens * entry.input_cache_miss_usd_per_1m_peak + usage.inputCacheHitTokens * entry.input_cache_hit_usd_per_1m_peak + usage.outputTokens * entry.output_usd_per_1m_peak) / 1_000_000;
|
|
84
|
+
return round6(cost);
|
|
85
|
+
}
|
|
86
|
+
export function buildNightlyRunRecord(input) {
|
|
87
|
+
const { schema_version, executed_at, real_llm, prompt_set_sha256, output_sha256, cost_usd } = input;
|
|
88
|
+
const core = {
|
|
89
|
+
schema_version,
|
|
90
|
+
executed_at,
|
|
91
|
+
real_llm,
|
|
92
|
+
prompt_set_sha256,
|
|
93
|
+
output_sha256,
|
|
94
|
+
cost_usd
|
|
95
|
+
};
|
|
96
|
+
if (!SHA256.test(prompt_set_sha256) || !SHA256.test(output_sha256)) {
|
|
97
|
+
throw new Error('nightly record hashes must be lowercase SHA-256 digests');
|
|
98
|
+
}
|
|
99
|
+
const record = {
|
|
100
|
+
...core,
|
|
101
|
+
run_key: `hmac-sha256:${createHash('sha256').update(canonical(core)).digest('hex')}`
|
|
102
|
+
};
|
|
103
|
+
parseNightlyRun(JSON.parse(JSON.stringify(record)), 0);
|
|
104
|
+
return record;
|
|
105
|
+
}
|
|
106
|
+
function activeModel() {
|
|
107
|
+
return process.env['LLM_MODEL'] ?? process.env['DEEPSEEK_MODEL'] ?? 'MiniMax-M2';
|
|
108
|
+
}
|
|
109
|
+
function arg(name) {
|
|
110
|
+
const index = process.argv.indexOf(name);
|
|
111
|
+
return index >= 0 ? process.argv[index + 1] : undefined;
|
|
112
|
+
}
|
|
113
|
+
export async function runNightlyEvidence(options) {
|
|
114
|
+
const clock = options.clock ?? (()=>new Date());
|
|
115
|
+
const llmKey = process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY'];
|
|
116
|
+
if (!options.dryRun && !llmKey) {
|
|
117
|
+
return {
|
|
118
|
+
state: 'waiting_external_evidence',
|
|
119
|
+
record: null,
|
|
120
|
+
reason: 'no real LLM credential (LLM_API_KEY/DEEPSEEK_API_KEY); waiting & backoff & no-spend per issue #22 停机纪律'
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const promptSet = loadPromptSet('data/m3-nightly-prompts.json');
|
|
124
|
+
const priceTable = loadPriceTable('data/llm-price-table.json');
|
|
125
|
+
const packPath = join('..', 'data', 'flights_2026.json');
|
|
126
|
+
const solvePort = realtimeSolvePort(solveUnified);
|
|
127
|
+
const port = options.dryRun ? createMockLlm(packPath) : createOpenAICompatLlm(packPath);
|
|
128
|
+
const state = newState();
|
|
129
|
+
const history = [];
|
|
130
|
+
for (const turn of promptSet.turns){
|
|
131
|
+
const { reply } = await runTurn(state, turn, port, [
|
|
132
|
+
...history
|
|
133
|
+
], solvePort);
|
|
134
|
+
history.push({
|
|
135
|
+
role: 'user',
|
|
136
|
+
text: turn
|
|
137
|
+
}, {
|
|
138
|
+
role: 'assistant',
|
|
139
|
+
text: reply
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const promptSetSha = sha256Of(promptSet);
|
|
143
|
+
const turnsTranscript = promptSet.turns.map((turn, index)=>({
|
|
144
|
+
turn_index: index,
|
|
145
|
+
user: turn,
|
|
146
|
+
reply: history[index * 2 + 1]?.text ?? ''
|
|
147
|
+
}));
|
|
148
|
+
const outputSha = sha256Of(turnsTranscript);
|
|
149
|
+
let record;
|
|
150
|
+
if (options.dryRun) {
|
|
151
|
+
record = buildNightlyRunRecord({
|
|
152
|
+
schema_version: 'gotry_m3_nightly_run_v1',
|
|
153
|
+
executed_at: clock().toISOString(),
|
|
154
|
+
real_llm: false,
|
|
155
|
+
prompt_set_sha256: promptSetSha,
|
|
156
|
+
output_sha256: outputSha,
|
|
157
|
+
cost_usd: 0
|
|
158
|
+
});
|
|
159
|
+
return {
|
|
160
|
+
state: 'dry_run',
|
|
161
|
+
record,
|
|
162
|
+
reason: 'dry-run exercises the pipeline against mock LLM; no evidence written'
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const usage = port.usage;
|
|
166
|
+
if (!usage) throw new Error('real LLM port did not expose usage tracker (dsh-llm regression) — refusing to write cost evidence');
|
|
167
|
+
if (usage.calls !== promptSet.turns.length) {
|
|
168
|
+
throw new Error(`usage calls ${usage.calls} != expected turns ${promptSet.turns.length} — fail-closed, no evidence written`);
|
|
169
|
+
}
|
|
170
|
+
const costUsd = priceRunCost(activeModel(), usage, priceTable);
|
|
171
|
+
record = buildNightlyRunRecord({
|
|
172
|
+
schema_version: 'gotry_m3_nightly_run_v1',
|
|
173
|
+
executed_at: clock().toISOString(),
|
|
174
|
+
real_llm: true,
|
|
175
|
+
prompt_set_sha256: promptSetSha,
|
|
176
|
+
output_sha256: outputSha,
|
|
177
|
+
cost_usd: costUsd
|
|
178
|
+
});
|
|
179
|
+
mkdirSync(options.evidenceRoot, {
|
|
180
|
+
recursive: true
|
|
181
|
+
});
|
|
182
|
+
appendFileSync(join(options.evidenceRoot, 'cohort.jsonl'), `${JSON.stringify(record)}\n`, 'utf8');
|
|
183
|
+
const budget = Number(process.env['GOTRY_NIGHTLY_BUDGET_USD'] ?? DEFAULT_BUDGET_USD);
|
|
184
|
+
const overBudget = Number.isFinite(budget) && budget > 0 ? costUsd > budget : false;
|
|
185
|
+
return {
|
|
186
|
+
state: 'evidence_written',
|
|
187
|
+
record,
|
|
188
|
+
cost_over_budget: overBudget,
|
|
189
|
+
reason: overBudget ? `cost_usd ${costUsd} exceeds GOTRY_NIGHTLY_BUDGET_USD ${budget} — backoff suggested` : undefined
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function output(payload, asJson) {
|
|
193
|
+
if (asJson) console.log(JSON.stringify(payload));
|
|
194
|
+
else {
|
|
195
|
+
console.log(`nightly-evidence state: ${payload.state}${payload.error ? ` — ${payload.error}` : ''}`);
|
|
196
|
+
if (payload.record) console.log(JSON.stringify(payload.record, null, 2));
|
|
197
|
+
if (payload.reason) console.log(payload.reason);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function main() {
|
|
201
|
+
if (!process.argv.includes('--no-env-file')) {
|
|
202
|
+
try {
|
|
203
|
+
for (const line of readFileSync(join('..', '.env'), 'utf-8').split('\n')){
|
|
204
|
+
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
205
|
+
if (m && !process.env[m[1]]) process.env[m[1]] = m[2].trim();
|
|
206
|
+
}
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
const asJson = (arg('--format') ?? 'markdown') === 'json';
|
|
210
|
+
const evidenceRoot = arg('--evidence-root') ?? 'gotry-state/evidence/m3';
|
|
211
|
+
const dryRun = process.argv.includes('--dry-run');
|
|
212
|
+
try {
|
|
213
|
+
const result = await runNightlyEvidence({
|
|
214
|
+
evidenceRoot,
|
|
215
|
+
dryRun
|
|
216
|
+
});
|
|
217
|
+
output(result, asJson);
|
|
218
|
+
if (result.cost_over_budget) process.exitCode = 3;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
output({
|
|
221
|
+
state: 'error',
|
|
222
|
+
record: null,
|
|
223
|
+
error: error.message
|
|
224
|
+
}, asJson);
|
|
225
|
+
process.exitCode = 1;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (process.argv[1]?.endsWith('nightly-evidence.ts')) {
|
|
229
|
+
await main();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/nightly-evidence.ts
|
package/dist/scripts/smoke.js
CHANGED
|
@@ -410,6 +410,89 @@ async function main() {
|
|
|
410
410
|
}
|
|
411
411
|
console.log('login tool: registered; 登录=外部网站+用户自己的浏览器,gotry 只读票据名(0 值过手)语义钉死');
|
|
412
412
|
}
|
|
413
|
+
{
|
|
414
|
+
const { writeFileSync } = await import('node:fs');
|
|
415
|
+
const { listArtifacts, readArtifact } = await import('../capabilities/artifacts.js');
|
|
416
|
+
const { ensureLedger } = await import('../src/state-ledger.js');
|
|
417
|
+
const cwdDir = mkdtempSync(join(tmpdir(), 'gotry-artifacts-cwd-'));
|
|
418
|
+
const twelve = Array.from({
|
|
419
|
+
length: 12
|
|
420
|
+
}, (_, i)=>`第 ${i + 1} 行`).join('\n');
|
|
421
|
+
writeFileSync(join(cwdDir, 'trip-2027-probe.md'), `# 行程·产物探针\n${twelve}\n`);
|
|
422
|
+
const ledger = ensureLedger(smokeRoot);
|
|
423
|
+
ledger.createWorkflowRun({
|
|
424
|
+
id: 'art-probe-1',
|
|
425
|
+
goal: '行程·产物探针(账本)',
|
|
426
|
+
ticket: {
|
|
427
|
+
objective: 'probe'
|
|
428
|
+
},
|
|
429
|
+
state: {}
|
|
430
|
+
});
|
|
431
|
+
ledger.settleWorkflowRun('art-probe-1', '# 交付·账本权威\nD1 大理\nD2 洱海');
|
|
432
|
+
const listTool = byName('gotry_artifacts_list');
|
|
433
|
+
const ledList = await listTool.execute({
|
|
434
|
+
query: {}
|
|
435
|
+
}, null);
|
|
436
|
+
const run = ledList.artifacts?.find((a)=>a.id === 'art-probe-1');
|
|
437
|
+
if (!ledList.ok || !run || run.source !== 'async-run' || run.status !== 'settled') {
|
|
438
|
+
throw new Error(`FAIL: artifacts list 应发现账本已交付工单,实际:${JSON.stringify(ledList).slice(0, 200)}`);
|
|
439
|
+
}
|
|
440
|
+
const direct = await listArtifacts({
|
|
441
|
+
stateRoot: smokeRoot,
|
|
442
|
+
cwd: cwdDir
|
|
443
|
+
});
|
|
444
|
+
if (!direct.artifacts.some((a)=>a.source === 'cwd-file' && a.id === 'trip-2027-probe.md')) {
|
|
445
|
+
throw new Error(`FAIL: artifacts list 应发现 cwd 顶层 md,实际:${JSON.stringify(direct.artifacts.map((a)=>a.id))}`);
|
|
446
|
+
}
|
|
447
|
+
const readTool = byName('gotry_artifacts_read');
|
|
448
|
+
const r1 = await readTool.execute({
|
|
449
|
+
query: {
|
|
450
|
+
path: 'art-probe-1'
|
|
451
|
+
}
|
|
452
|
+
}, null);
|
|
453
|
+
if (!r1.ok || r1.lines?.[0]?.number !== 1 || !r1.lines?.[0]?.text.includes('交付·账本权威') || r1.lang !== 'markdown') {
|
|
454
|
+
throw new Error(`FAIL: 裸工单 id 应从账本读出行号视图,实际:${JSON.stringify(r1).slice(0, 200)}`);
|
|
455
|
+
}
|
|
456
|
+
const view = readTool.presentResult?.({
|
|
457
|
+
query: {
|
|
458
|
+
path: 'art-probe-1'
|
|
459
|
+
}
|
|
460
|
+
}, r1);
|
|
461
|
+
if (view?.card !== 'read' || view.path !== r1.path || view.offset !== 1 || view.lines?.length !== r1.lines?.length || view.totalLines !== r1.totalLines || view.lang !== 'markdown') {
|
|
462
|
+
throw new Error(`FAIL: read 卡字段不齐,实际:${JSON.stringify(view).slice(0, 200)}`);
|
|
463
|
+
}
|
|
464
|
+
const r2 = await readArtifact({
|
|
465
|
+
stateRoot: smokeRoot,
|
|
466
|
+
cwd: cwdDir,
|
|
467
|
+
path: 'trip-2027-probe.md',
|
|
468
|
+
offset: 10,
|
|
469
|
+
limit: 5
|
|
470
|
+
});
|
|
471
|
+
if (!r2.ok || r2.lines[0].number !== 10 || r2.windowed !== false) {
|
|
472
|
+
throw new Error(`FAIL: 行窗口 12 行文件 offset=10 应从行号 10 起,实际:${JSON.stringify(r2.ok ? {
|
|
473
|
+
o: r2.offset,
|
|
474
|
+
first: r2.lines[0]?.number
|
|
475
|
+
} : r2)}`);
|
|
476
|
+
}
|
|
477
|
+
if (r2.lines.length !== 5) throw new Error(`FAIL: 14 行文件 offset=10/limit=5 窗口应恰 5 行(10-14),实际 ${r2.lines.length}`);
|
|
478
|
+
const badPath = await readArtifact({
|
|
479
|
+
stateRoot: smokeRoot,
|
|
480
|
+
cwd: cwdDir,
|
|
481
|
+
path: '../../../../../etc/passwd'
|
|
482
|
+
});
|
|
483
|
+
if (badPath.ok) throw new Error('FAIL: 越界路径必须被拒');
|
|
484
|
+
const badExt = await readArtifact({
|
|
485
|
+
stateRoot: smokeRoot,
|
|
486
|
+
cwd: cwdDir,
|
|
487
|
+
path: 'gotry-state/gotry-state.db'
|
|
488
|
+
});
|
|
489
|
+
if (badExt.ok) throw new Error('FAIL: 白名单外扩展名(.db)必须被拒');
|
|
490
|
+
rmSync(cwdDir, {
|
|
491
|
+
recursive: true,
|
|
492
|
+
force: true
|
|
493
|
+
});
|
|
494
|
+
console.log(`artifacts: ledger run + cwd md discovered; read window(${r2.ok ? r2.lines.length : '?'} lines @10) + read card; path/ext guardrails hold`);
|
|
495
|
+
}
|
|
413
496
|
rmSync(smokeRoot, {
|
|
414
497
|
recursive: true,
|
|
415
498
|
force: true
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export const BOOKING_SAGA_SCHEMA = 'booking_saga_fsm.v1';
|
|
2
|
+
export const SAGA_STATUSES = [
|
|
3
|
+
'pending',
|
|
4
|
+
'confirmed',
|
|
5
|
+
'compensated'
|
|
6
|
+
];
|
|
7
|
+
export const SAGA_EDGES = [
|
|
8
|
+
{
|
|
9
|
+
from: 'none',
|
|
10
|
+
trigger: 'propose',
|
|
11
|
+
to: 'pending',
|
|
12
|
+
event: 'write.pending',
|
|
13
|
+
guard: 'L2 只登记不执行;idem_key UNIQUE(重复提议 no-op)'
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
from: 'pending',
|
|
17
|
+
trigger: 'confirm',
|
|
18
|
+
to: 'confirmed',
|
|
19
|
+
event: 'write.confirmed',
|
|
20
|
+
guard: 'L3 具名 seam 确认,必携 receipt;已确认不可再确认'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
from: 'pending',
|
|
24
|
+
trigger: 'compensate',
|
|
25
|
+
to: 'compensated',
|
|
26
|
+
event: 'write.compensated',
|
|
27
|
+
guard: '外部写未发生,取消即终态(无补偿动作)'
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
from: 'confirmed',
|
|
31
|
+
trigger: 'compensate',
|
|
32
|
+
to: 'compensated',
|
|
33
|
+
event: 'write.compensated',
|
|
34
|
+
guard: '已发生副作用的 saga 补偿(退改),receipt 保留(COALESCE)'
|
|
35
|
+
}
|
|
36
|
+
];
|
|
37
|
+
export const SAGA_REJECTIONS = [
|
|
38
|
+
{
|
|
39
|
+
from: 'none',
|
|
40
|
+
trigger: 'confirm',
|
|
41
|
+
reason: 'missing-subject'
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
from: 'none',
|
|
45
|
+
trigger: 'compensate',
|
|
46
|
+
reason: 'missing-subject'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
from: 'pending',
|
|
50
|
+
trigger: 'propose',
|
|
51
|
+
reason: 'idem-exists'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
from: 'confirmed',
|
|
55
|
+
trigger: 'propose',
|
|
56
|
+
reason: 'idem-exists'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
from: 'confirmed',
|
|
60
|
+
trigger: 'confirm',
|
|
61
|
+
reason: 'already-confirmed'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
from: 'compensated',
|
|
65
|
+
trigger: 'propose',
|
|
66
|
+
reason: 'idem-exists'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
from: 'compensated',
|
|
70
|
+
trigger: 'confirm',
|
|
71
|
+
reason: 'absorbed-compensated'
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
from: 'compensated',
|
|
75
|
+
trigger: 'compensate',
|
|
76
|
+
reason: 'already-compensated'
|
|
77
|
+
}
|
|
78
|
+
];
|
|
79
|
+
export function resolveSagaTrigger(origin, trigger) {
|
|
80
|
+
const edge = SAGA_EDGES.find((e)=>e.from === origin && e.trigger === trigger);
|
|
81
|
+
if (edge) return {
|
|
82
|
+
ok: true,
|
|
83
|
+
from: origin,
|
|
84
|
+
trigger,
|
|
85
|
+
to: edge.to,
|
|
86
|
+
event: edge.event,
|
|
87
|
+
guard: edge.guard,
|
|
88
|
+
reason: null
|
|
89
|
+
};
|
|
90
|
+
const rejection = SAGA_REJECTIONS.find((e)=>e.from === origin && e.trigger === trigger);
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
from: origin,
|
|
94
|
+
trigger,
|
|
95
|
+
to: null,
|
|
96
|
+
event: null,
|
|
97
|
+
guard: null,
|
|
98
|
+
reason: rejection.reason
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export const SAGA_TRIGGER_EVENT = {
|
|
102
|
+
propose: 'write.pending',
|
|
103
|
+
confirm: 'write.confirmed',
|
|
104
|
+
compensate: 'write.compensated'
|
|
105
|
+
};
|
|
106
|
+
export function triggerOfEventKind(kind) {
|
|
107
|
+
if (kind === 'write.pending') return 'propose';
|
|
108
|
+
if (kind === 'write.confirmed') return 'confirm';
|
|
109
|
+
if (kind === 'write.compensated') return 'compensate';
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
export function sagaTraceViolations(events) {
|
|
113
|
+
const violations = [];
|
|
114
|
+
let stage = 'none';
|
|
115
|
+
for (const ev of events){
|
|
116
|
+
const trigger = triggerOfEventKind(ev.kind);
|
|
117
|
+
if (!trigger) continue;
|
|
118
|
+
const verdict = resolveSagaTrigger(stage, trigger);
|
|
119
|
+
const at = `seq=${ev.seq ?? '?'} ${ev.kind}`;
|
|
120
|
+
if (!verdict.ok) {
|
|
121
|
+
violations.push(`${at}: 非法(${stage} --${trigger}--> 拒绝 ${verdict.reason})`);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
stage = verdict.to;
|
|
125
|
+
if (trigger === 'confirm' && (ev.receipt === undefined || ev.receipt === null || ev.receipt === '')) {
|
|
126
|
+
violations.push(`${at}: receipt 为空(L3 具名确认必携外部回执)`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return violations;
|
|
130
|
+
}
|
|
131
|
+
export function sagaLegalPaths() {
|
|
132
|
+
return [
|
|
133
|
+
[
|
|
134
|
+
'propose'
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
'propose',
|
|
138
|
+
'compensate'
|
|
139
|
+
],
|
|
140
|
+
[
|
|
141
|
+
'propose',
|
|
142
|
+
'confirm'
|
|
143
|
+
],
|
|
144
|
+
[
|
|
145
|
+
'propose',
|
|
146
|
+
'confirm',
|
|
147
|
+
'compensate'
|
|
148
|
+
]
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
//# sourceURL=/Users/bytedance/work/gotry/ts/src/booking-saga.ts
|
package/dist/src/dsh-llm.js
CHANGED
|
@@ -3,7 +3,17 @@ import { buildTimeAnchor } from './time-anchor.js';
|
|
|
3
3
|
import { buildSlotSystem, flagExpiredSlots, normalizeExtraction } from './travel-slots.js';
|
|
4
4
|
const model = ()=>process.env['LLM_MODEL'] ?? process.env['DEEPSEEK_MODEL'] ?? 'MiniMax-M2';
|
|
5
5
|
const base = ()=>(process.env['LLM_BASE_URL'] ?? process.env['DEEPSEEK_BASE_URL'] ?? 'https://api.minimax.io/v1').replace(/\/$/, '');
|
|
6
|
-
|
|
6
|
+
function emptyUsage() {
|
|
7
|
+
return {
|
|
8
|
+
calls: 0,
|
|
9
|
+
inputTokens: 0,
|
|
10
|
+
outputTokens: 0,
|
|
11
|
+
inputCacheHitTokens: 0,
|
|
12
|
+
inputCacheMissTokens: 0,
|
|
13
|
+
responsesMissingUsage: 0
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async function chat(messages, json, usage) {
|
|
7
17
|
const key = process.env['LLM_API_KEY'] ?? process.env['DEEPSEEK_API_KEY'];
|
|
8
18
|
if (!key) throw new Error('LLM_API_KEY 未设置(兼容 DEEPSEEK_API_KEY 别名)——真 LLM 路径不可用,请回退 mock(ADR-8)');
|
|
9
19
|
const res = await fetch(`${base()}/chat/completions`, {
|
|
@@ -25,6 +35,18 @@ async function chat(messages, json) {
|
|
|
25
35
|
});
|
|
26
36
|
if (!res.ok) throw new Error(`llm ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
27
37
|
const data = await res.json();
|
|
38
|
+
if (usage) {
|
|
39
|
+
usage.calls += 1;
|
|
40
|
+
const u = data.usage;
|
|
41
|
+
if (u && Number.isFinite(u.prompt_tokens) && Number.isFinite(u.completion_tokens)) {
|
|
42
|
+
usage.inputTokens += u.prompt_tokens ?? 0;
|
|
43
|
+
usage.outputTokens += u.completion_tokens ?? 0;
|
|
44
|
+
usage.inputCacheHitTokens += u.prompt_cache_hit_tokens ?? 0;
|
|
45
|
+
usage.inputCacheMissTokens += u.prompt_cache_miss_tokens ?? u.prompt_tokens ?? 0;
|
|
46
|
+
} else {
|
|
47
|
+
usage.responsesMissingUsage += 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
28
50
|
const raw = data.choices[0]?.message?.content ?? '';
|
|
29
51
|
return raw.replace(/<think>[\s\S]*?<\/think>/g, '').trim() || raw;
|
|
30
52
|
}
|
|
@@ -50,6 +72,7 @@ const SKELETON_SYSTEM = `你是行程骨架抽取器。从对话中抽取行程
|
|
|
50
72
|
scenario 判定:「洱海/大理/千岛湖/太湖+选目的地」→erhai(候选集);「普吉/workation/远程办公+多城链」→workation(五段链);「云南/大理丽江」→yunnan;不确定→generic。只输出 JSON。`;
|
|
51
73
|
export function createOpenAICompatLlm(flightPackPath, clock = ()=>new Date()) {
|
|
52
74
|
const pack = flightPackPath;
|
|
75
|
+
const usage = emptyUsage();
|
|
53
76
|
const historyText = (h)=>h.map((t)=>`${t.role === 'user' ? '用户' : '助手'}: ${t.text}`).join('\n');
|
|
54
77
|
const anchorContext = ()=>`时间锚点卡:\n${buildTimeAnchor(clock()).card}`;
|
|
55
78
|
return {
|
|
@@ -63,7 +86,7 @@ export function createOpenAICompatLlm(flightPackPath, clock = ()=>new Date()) {
|
|
|
63
86
|
role: 'user',
|
|
64
87
|
content: `${anchorContext()}\n\n${historyText(history)}`
|
|
65
88
|
}
|
|
66
|
-
], true);
|
|
89
|
+
], true, usage);
|
|
67
90
|
const obj = parseJsonBlock(out);
|
|
68
91
|
if (!obj) return {
|
|
69
92
|
assumptions: []
|
|
@@ -91,7 +114,7 @@ export function createOpenAICompatLlm(flightPackPath, clock = ()=>new Date()) {
|
|
|
91
114
|
role: 'user',
|
|
92
115
|
content: `${context}\n\n${historyText(history)}`
|
|
93
116
|
}
|
|
94
|
-
], true);
|
|
117
|
+
], true, usage);
|
|
95
118
|
const skeleton = parseJsonBlock(out);
|
|
96
119
|
if (!skeleton || !Array.isArray(skeleton['segments']) || skeleton['segments'].length === 0) return null;
|
|
97
120
|
if (!pack) return null;
|
|
@@ -148,7 +171,7 @@ export function createOpenAICompatLlm(flightPackPath, clock = ()=>new Date()) {
|
|
|
148
171
|
role: 'user',
|
|
149
172
|
content: historyText(history)
|
|
150
173
|
}
|
|
151
|
-
], true);
|
|
174
|
+
], true, usage);
|
|
152
175
|
const obj = parseJsonBlock(out);
|
|
153
176
|
if (!obj) return null;
|
|
154
177
|
const ext = normalizeExtraction(obj, history.map((t)=>t.text).join('\n'));
|
|
@@ -164,9 +187,10 @@ export function createOpenAICompatLlm(flightPackPath, clock = ()=>new Date()) {
|
|
|
164
187
|
role: 'user',
|
|
165
188
|
content: `【${q.key}】${q.text}(为什么问:${q.why})`
|
|
166
189
|
}
|
|
167
|
-
], false);
|
|
190
|
+
], false, usage);
|
|
168
191
|
return out.trim() || `【${q.key}】${q.text}`;
|
|
169
|
-
}
|
|
192
|
+
},
|
|
193
|
+
usage
|
|
170
194
|
};
|
|
171
195
|
}
|
|
172
196
|
|