@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.20

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 (176) hide show
  1. package/README.md +42 -16
  2. package/README.zh-CN.md +37 -15
  3. package/bin/gotry-bootstrap.js +309 -30
  4. package/bin/gotry-inner.js +74 -23
  5. package/bin/gotry-runtime-resolution.d.ts +1 -1
  6. package/bin/gotry-runtime-resolution.js +7 -8
  7. package/cordis.gotry-patch.yml +22 -8
  8. package/data/stations-12306-verify.json +524 -0
  9. package/dist/capabilities/agent-reach.js +4 -4
  10. package/dist/capabilities/anything.js +62 -29
  11. package/dist/capabilities/channel-health.js +103 -0
  12. package/dist/capabilities/channel-registry.js +213 -0
  13. package/dist/capabilities/doctor.js +307 -0
  14. package/dist/capabilities/effect.js +178 -5
  15. package/dist/capabilities/flyai.js +13 -0
  16. package/dist/capabilities/hbcli.js +28 -2
  17. package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
  18. package/dist/capabilities/session/adapters/rail-12306.js +285 -0
  19. package/dist/capabilities/session/extension-bridge.js +14 -1
  20. package/dist/capabilities/session/extension-channel.js +4 -2
  21. package/dist/capabilities/session/extension-distribution.js +1 -1
  22. package/dist/capabilities/session-search.js +311 -0
  23. package/dist/capabilities/visa-policy.js +126 -0
  24. package/dist/scripts/agent-planning-turn-deadline-e2e.js +305 -0
  25. package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
  26. package/dist/scripts/anything-tests.js +70 -21
  27. package/dist/scripts/benchmark-environment-bridge-e2e.js +139 -37
  28. package/dist/scripts/benchmark-environment-bridge-tests.js +1099 -306
  29. package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
  30. package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
  31. package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
  32. package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
  33. package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
  34. package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
  35. package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
  36. package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
  37. package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
  38. package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
  39. package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
  40. package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
  41. package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
  42. package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
  43. package/dist/scripts/bootstrap-tests.js +113 -2
  44. package/dist/scripts/build-metrics-report.js +339 -0
  45. package/dist/scripts/channel-probe-tests.js +148 -0
  46. package/dist/scripts/channel-probe.js +252 -0
  47. package/dist/scripts/channel-registry-tests.js +262 -0
  48. package/dist/scripts/doctor-tests.js +175 -0
  49. package/dist/scripts/effect-tests.js +274 -1
  50. package/dist/scripts/extension-tests.js +129 -11
  51. package/dist/scripts/fact-gate-tests.js +91 -2
  52. package/dist/scripts/flyai-tests.js +17 -1
  53. package/dist/scripts/hbcli-e2e-tests.js +1 -3
  54. package/dist/scripts/hbcli-tests.js +38 -1
  55. package/dist/scripts/metrics-report-tests.js +288 -0
  56. package/dist/scripts/persona-surface-guard-tests.js +19 -0
  57. package/dist/scripts/session-tests.js +401 -1
  58. package/dist/scripts/smoke.js +100 -83
  59. package/dist/scripts/turn-handoff-collect-tests.js +216 -0
  60. package/dist/scripts/turn-handoff-collect.js +189 -0
  61. package/dist/scripts/turn-policy-tests.js +56 -0
  62. package/dist/scripts/typed-contract-canary.js +261 -0
  63. package/dist/scripts/visa-policy-tests.js +75 -0
  64. package/dist/scripts/wish-channel-gate-tests.js +111 -0
  65. package/dist/src/artifact-gate.js +91 -3
  66. package/dist/src/benchmark-agent-conformance.js +39 -16
  67. package/dist/src/benchmark-environment-bridge.js +316 -52
  68. package/dist/src/bookable-facts.js +62 -3
  69. package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
  70. package/dist/src/booking-surface/canonical-schema.js +8 -22
  71. package/dist/src/booking-surface/contracts.js +70 -19
  72. package/dist/src/booking-surface/dsh-planner.js +310 -177
  73. package/dist/src/booking-surface/dsh-plugin.js +3 -3
  74. package/dist/src/booking-surface/index.js +1 -5
  75. package/dist/src/booking-surface/profile.js +1 -1
  76. package/dist/src/booking-surface/runtime.js +1682 -247
  77. package/dist/src/booking-surface/server.js +371 -185
  78. package/dist/src/booking-surface/startup.js +17 -27
  79. package/dist/src/booking-surface/validation.js +270 -760
  80. package/dist/src/index.js +739 -150
  81. package/dist/src/turn-deadline.js +292 -0
  82. package/dist/src/turn-policy.js +146 -0
  83. package/dist/src/wish-pool.js +5 -0
  84. package/extension/background.js +23 -4
  85. package/extension/content-main.js +47 -9
  86. package/extension/manifest.json +27 -9
  87. package/package.json +7 -26
  88. package/schemas/booking.surface.schema.json +2593 -0
  89. package/ts/capabilities/agent-reach.ts +4 -4
  90. package/ts/capabilities/anything.ts +89 -38
  91. package/ts/capabilities/flyai.ts +21 -3
  92. package/ts/capabilities/hbcli.ts +53 -4
  93. package/ts/capabilities/session/action-cache.ts +1 -1
  94. package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
  95. package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
  96. package/ts/capabilities/session/extension-bridge.ts +32 -2
  97. package/ts/capabilities/session/extension-channel.ts +9 -8
  98. package/ts/capabilities/session/extension-distribution.ts +3 -1
  99. package/ts/capabilities/session/health-watch.ts +1 -1
  100. package/ts/capabilities/session/read-guard.ts +1 -1
  101. package/ts/capabilities/session/wizard.ts +1 -1
  102. package/ts/capabilities/session-search.ts +323 -1
  103. package/ts/dsh-runtime/vendor/README.md +60 -0
  104. package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
  105. package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
  106. package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
  107. package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
  108. package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
  109. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
  110. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
  111. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
  112. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
  113. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
  114. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
  115. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
  116. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
  117. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
  118. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
  119. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
  120. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
  121. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
  122. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
  123. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
  124. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
  125. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
  126. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
  127. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
  128. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
  129. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
  130. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
  131. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
  132. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
  133. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
  134. package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
  135. package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
  136. package/ts/package.json +9 -0
  137. package/ts/scripts/turn-handoff-collect.ts +178 -0
  138. package/ts/src/artifact-gate.ts +82 -2
  139. package/ts/src/benchmark-agent-conformance.ts +54 -15
  140. package/ts/src/benchmark-environment-bridge.ts +234 -46
  141. package/ts/src/bookable-facts.ts +109 -5
  142. package/ts/src/booking-saga.ts +1 -1
  143. package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
  144. package/ts/src/booking-surface/canonical-schema.js +8 -22
  145. package/ts/src/booking-surface/contracts.ts +278 -239
  146. package/ts/src/booking-surface/dsh-planner.ts +333 -194
  147. package/ts/src/booking-surface/dsh-plugin.js +3 -3
  148. package/ts/src/booking-surface/index.ts +1 -5
  149. package/ts/src/booking-surface/profile.ts +11 -11
  150. package/ts/src/booking-surface/runtime.ts +1351 -351
  151. package/ts/src/booking-surface/server.ts +282 -175
  152. package/ts/src/booking-surface/startup.ts +36 -48
  153. package/ts/src/booking-surface/validation.ts +194 -442
  154. package/ts/src/contracts.ts +1 -1
  155. package/ts/src/index.ts +492 -172
  156. package/ts/src/loop.ts +1 -1
  157. package/ts/src/state-ledger.ts +1 -1
  158. package/ts/src/turn-deadline.ts +365 -0
  159. package/ts/src/turn-policy.ts +154 -0
  160. package/ts/src/wish-pool.ts +17 -4
  161. package/dist/scripts/agent-planning-budget-e2e.js +0 -227
  162. package/dist/scripts/agent-planning-budget-tests.js +0 -173
  163. package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
  164. package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
  165. package/dist/src/booking-surface/contracts-v2.js +0 -89
  166. package/dist/src/booking-surface/runtime-v2.js +0 -1771
  167. package/dist/src/booking-surface/server-v2.js +0 -334
  168. package/dist/src/booking-surface/validation-v2.js +0 -319
  169. package/dist/src/tool-budget.js +0 -136
  170. package/schemas/booking.surface.v1.schema.json +0 -927
  171. package/schemas/booking.surface.v2.schema.json +0 -61
  172. package/ts/src/booking-surface/contracts-v2.ts +0 -118
  173. package/ts/src/booking-surface/runtime-v2.ts +0 -1466
  174. package/ts/src/booking-surface/server-v2.ts +0 -247
  175. package/ts/src/booking-surface/validation-v2.ts +0 -205
  176. package/ts/src/tool-budget.ts +0 -165
@@ -0,0 +1,305 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createServer } from 'node:http';
3
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
+ const ROOT = join(import.meta.dirname, '..', '..');
8
+ const TS_DIR = join(import.meta.dirname, '..');
9
+ const TIMEOUT_MS = 90_000;
10
+ const TOOL = 'gotry_artifacts_list';
11
+ const SOFT_MS = 5_000;
12
+ const HARD_MS = 10_000;
13
+ const RELAY_STALL_MS = HARD_MS + 1_000;
14
+ const HANDOFF = 'TURN_DEADLINE_HANDOFF';
15
+ const HANDOFF_SCHEMA = 'gotry_turn_handoff.v1';
16
+ const DEEP_MESSAGE = '2026年我还有6天IRW额度,我准备再请几天假,在国内待个十几天。' + '其中10.3要去湖南衡阳参加同学婚礼。请根据我的实际情况和偏好,给我安排行程';
17
+ function sse(data) {
18
+ return `data: ${JSON.stringify(data)}\n\n`;
19
+ }
20
+ function toolResponse(idSuffix) {
21
+ const call = {
22
+ index: 0,
23
+ id: `deadline-call-${idSuffix}`,
24
+ type: 'function',
25
+ function: {
26
+ name: TOOL,
27
+ arguments: JSON.stringify({
28
+ query: {
29
+ limit: 1
30
+ }
31
+ })
32
+ }
33
+ };
34
+ return sse({
35
+ id: `deadline-${idSuffix}`,
36
+ object: 'chat.completion.chunk',
37
+ created: 1,
38
+ model: 'synthetic-deadline-model',
39
+ choices: [
40
+ {
41
+ index: 0,
42
+ delta: {
43
+ role: 'assistant',
44
+ tool_calls: [
45
+ call
46
+ ]
47
+ },
48
+ finish_reason: null
49
+ }
50
+ ]
51
+ }) + sse({
52
+ id: `deadline-${idSuffix}-finish`,
53
+ object: 'chat.completion.chunk',
54
+ created: 1,
55
+ model: 'synthetic-deadline-model',
56
+ choices: [
57
+ {
58
+ index: 0,
59
+ delta: {},
60
+ finish_reason: 'tool_calls'
61
+ }
62
+ ]
63
+ }) + 'data: [DONE]\n\n';
64
+ }
65
+ function textResponse(text, id = 'deadline-final') {
66
+ return sse({
67
+ id,
68
+ object: 'chat.completion.chunk',
69
+ created: 1,
70
+ model: 'synthetic-deadline-model',
71
+ choices: [
72
+ {
73
+ index: 0,
74
+ delta: {
75
+ role: 'assistant',
76
+ content: text
77
+ },
78
+ finish_reason: null
79
+ }
80
+ ]
81
+ }) + sse({
82
+ id: `${id}-finish`,
83
+ object: 'chat.completion.chunk',
84
+ created: 1,
85
+ model: 'synthetic-deadline-model',
86
+ choices: [
87
+ {
88
+ index: 0,
89
+ delta: {},
90
+ finish_reason: 'stop'
91
+ }
92
+ ]
93
+ }) + 'data: [DONE]\n\n';
94
+ }
95
+ async function startRelay() {
96
+ const bodies = [];
97
+ const plannerBodies = [];
98
+ const server = createServer((req, res)=>{
99
+ if (req.method !== 'POST' || !req.url?.endsWith('/chat/completions')) {
100
+ res.writeHead(404).end('not found');
101
+ return;
102
+ }
103
+ const chunks = [];
104
+ req.on('data', (chunk)=>chunks.push(Buffer.from(chunk)));
105
+ req.on('end', ()=>{
106
+ let body;
107
+ try {
108
+ body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
109
+ } catch {
110
+ body = {};
111
+ }
112
+ bodies.push(body);
113
+ res.writeHead(200, {
114
+ 'content-type': 'text/event-stream',
115
+ 'cache-control': 'no-cache'
116
+ });
117
+ if (toolNames(body).includes(TOOL)) {
118
+ plannerBodies.push(body);
119
+ const plannerIndex = plannerBodies.length;
120
+ const realToolMessages = (body.messages ?? []).filter((message)=>message.role === 'tool');
121
+ const alreadyHandedOff = realToolMessages.some((message)=>JSON.stringify(message).includes(HANDOFF));
122
+ const stall = !alreadyHandedOff && plannerIndex === 1 ? RELAY_STALL_MS : 0;
123
+ setTimeout(()=>{
124
+ if (!alreadyHandedOff && plannerIndex === 1) {
125
+ res.end(toolResponse(String(plannerIndex)));
126
+ } else {
127
+ res.end(textResponse('deadline E2E final answer'));
128
+ }
129
+ }, stall);
130
+ } else {
131
+ res.end(textResponse('deadline E2E final answer', 'deadline-auxiliary'));
132
+ }
133
+ });
134
+ });
135
+ await new Promise((resolve)=>server.listen(0, '127.0.0.1', resolve));
136
+ return {
137
+ port: server.address().port,
138
+ bodies,
139
+ plannerBodies,
140
+ close: ()=>new Promise((resolve)=>server.close(()=>resolve()))
141
+ };
142
+ }
143
+ function allToolMessages(bodies) {
144
+ return bodies.flatMap((body)=>(body.messages ?? []).filter((message)=>message.role === 'tool'));
145
+ }
146
+ function toolNames(body) {
147
+ return (body.tools ?? []).map((tool)=>{
148
+ const fn = tool.function;
149
+ return String(fn?.name ?? tool.name ?? '');
150
+ }).filter(Boolean);
151
+ }
152
+ async function main() {
153
+ for (const file of [
154
+ 'dist/src/index.js',
155
+ 'dist/capabilities/artifacts.js'
156
+ ]){
157
+ assert.ok(existsSync(join(ROOT, file)), `missing ${file}; run node scripts/build-dist.mjs first`);
158
+ }
159
+ const relay = await startRelay();
160
+ const dshHome = mkdtempSync(join(tmpdir(), 'gotry-deadline-dsh-home-'));
161
+ const childCwd = mkdtempSync(join(tmpdir(), 'gotry-deadline-cwd-'));
162
+ const handoffRoot = mkdtempSync(join(tmpdir(), 'gotry-deadline-handoff-'));
163
+ let exitCode = null;
164
+ let output = '';
165
+ const configuredBin = process.env.GOTRY_DEADLINE_E2E_BIN;
166
+ assert.ok(configuredBin && existsSync(configuredBin), 'GOTRY_DEADLINE_E2E_BIN must point to the clean installed-package binary');
167
+ try {
168
+ const executableArgs = [
169
+ DEEP_MESSAGE
170
+ ];
171
+ const childEnv = {
172
+ ...process.env
173
+ };
174
+ childEnv.DSH_HOME = dshHome;
175
+ childEnv.HOME = dshHome;
176
+ childEnv.LLM_API_KEY = 'synthetic-e2e-key';
177
+ childEnv.LLM_BASE_URL = 'http://127.0.0.1:' + relay.port + '/v1';
178
+ childEnv.LLM_MODEL = 'synthetic-deadline-model';
179
+ childEnv.GOTRY_LOCALE = 'zh-CN';
180
+ childEnv.GOTRY_TURN_DEADLINE_SOFT_MS = String(SOFT_MS);
181
+ childEnv.GOTRY_TURN_DEADLINE_HARD_MS = String(HARD_MS);
182
+ childEnv.GOTRY_TURN_HANDOFF_ROOT = handoffRoot;
183
+ delete childEnv.GOTRY_LLM_MODEL;
184
+ delete childEnv.GOTRY_FORCE_TURN_DEADLINE;
185
+ const child = spawn(configuredBin, executableArgs, {
186
+ cwd: childCwd,
187
+ env: childEnv,
188
+ stdio: [
189
+ 'ignore',
190
+ 'pipe',
191
+ 'pipe'
192
+ ]
193
+ });
194
+ child.stdout.on('data', (chunk)=>{
195
+ output += chunk.toString();
196
+ });
197
+ child.stderr.on('data', (chunk)=>{
198
+ output += chunk.toString();
199
+ });
200
+ exitCode = await new Promise((resolve)=>{
201
+ const timer = setTimeout(()=>{
202
+ child.kill('SIGKILL');
203
+ resolve(null);
204
+ }, TIMEOUT_MS);
205
+ child.once('exit', (code)=>{
206
+ clearTimeout(timer);
207
+ resolve(code);
208
+ });
209
+ });
210
+ } finally{
211
+ await new Promise((resolve)=>setTimeout(resolve, 200));
212
+ }
213
+ assert.equal(exitCode, 0, `headless exit=${exitCode}; output tail=${output.slice(-1200)}`);
214
+ assert.ok(relay.plannerBodies.length >= 1, `expected at least one tool-enabled planner request, got ${relay.plannerBodies.length}; output tail=${output.slice(-12_000)}`);
215
+ const toolMessages = allToolMessages(relay.bodies);
216
+ const toolMessageDiagnostic = JSON.stringify(toolMessages).slice(-12_000);
217
+ const handoffResults = toolMessages.filter((message)=>JSON.stringify(message).includes(HANDOFF));
218
+ const successfulResults = toolMessages.filter((message)=>!JSON.stringify(message).includes(HANDOFF));
219
+ assert.ok(handoffResults.length >= 1, `expected at least one handoff result; messages=${toolMessageDiagnostic}`);
220
+ assert.equal(successfulResults.length, 0, `no tool body should run past the hard deadline; messages=${toolMessageDiagnostic}`);
221
+ assert.match(JSON.stringify(handoffResults[0]), /th-/, 'handoff result carries the ticket id');
222
+ const ticketDir = join(handoffRoot, 'gotry-state', 'turn-handoffs');
223
+ assert.ok(existsSync(ticketDir), `ticket dir missing: ${ticketDir}`);
224
+ const ticketFiles = readdirSync(ticketDir).filter((name)=>name.endsWith('.json'));
225
+ assert.equal(ticketFiles.length, 1, `expected exactly one handoff ticket, got ${ticketFiles.join(', ')}`);
226
+ const ticket = JSON.parse(readFileSync(join(ticketDir, ticketFiles[0]), 'utf8'));
227
+ assert.equal(ticket['schema'], HANDOFF_SCHEMA);
228
+ assert.equal(ticket['status'], 'open');
229
+ assert.ok(String(ticket['userMessage']).includes('婚礼'), 'ticket carries the verbatim deep-planning ask');
230
+ assert.ok(String(ticket['etaLabel']).includes('约 1 小时'));
231
+ const finalRequest = relay.bodies.at(-1);
232
+ const finalNames = toolNames(finalRequest);
233
+ assert.ok(!finalNames.includes(TOOL), `final request inherited GoTry tool: ${finalNames.join(', ')}`);
234
+ assert.match(output, /deadline E2E final answer/);
235
+ {
236
+ const ticketId = String(ticket['id']);
237
+ const collector = spawn(process.execPath, [
238
+ join(TS_DIR, 'node_modules', 'tsx', 'dist', 'cli.mjs'),
239
+ join(TS_DIR, 'scripts', 'turn-handoff-collect.ts'),
240
+ ticketId,
241
+ handoffRoot
242
+ ], {
243
+ cwd: TS_DIR,
244
+ env: {
245
+ ...process.env,
246
+ GOTRY_HANDOFF_PLANNER_BIN: configuredBin,
247
+ LLM_API_KEY: 'synthetic-e2e-key',
248
+ LLM_BASE_URL: 'http://127.0.0.1:' + relay.port + '/v1',
249
+ LLM_MODEL: 'synthetic-deadline-model',
250
+ GOTRY_LOCALE: 'zh-CN'
251
+ },
252
+ stdio: [
253
+ 'ignore',
254
+ 'pipe',
255
+ 'pipe'
256
+ ]
257
+ });
258
+ let collectorOut = '';
259
+ let collectorErr = '';
260
+ collector.stdout.on('data', (chunk)=>{
261
+ collectorOut += chunk.toString();
262
+ });
263
+ collector.stderr.on('data', (chunk)=>{
264
+ collectorErr += chunk.toString();
265
+ });
266
+ const collectorCode = await new Promise((resolve)=>{
267
+ const timer = setTimeout(()=>{
268
+ collector.kill('SIGKILL');
269
+ resolve(null);
270
+ }, 90_000);
271
+ collector.once('exit', (code)=>{
272
+ clearTimeout(timer);
273
+ resolve(code);
274
+ });
275
+ });
276
+ assert.equal(collectorCode, 0, `collector exit=${collectorCode}; stderr tail=${collectorErr.slice(-800)}; stdout tail=${collectorOut.slice(-400)}`);
277
+ const terminal = JSON.parse(collectorOut.trim().split('\n').at(-1));
278
+ assert.equal(terminal['schema'], 'gotry_turn_handoff_terminal.v1');
279
+ assert.equal(terminal['status'], 'succeeded');
280
+ assert.ok(String(terminal['deliverable_path']).includes(`${ticketId}.deliverable.md`));
281
+ const settled = JSON.parse(readFileSync(join(ticketDir, `${ticketId}.json`), 'utf8'));
282
+ assert.equal(settled['status'], 'settled');
283
+ assert.ok(settled['settledAt'], 'settledAt recorded');
284
+ const deliverable = readFileSync(join(ticketDir, `${ticketId}.deliverable.md`), 'utf8');
285
+ assert.ok(deliverable.includes('deadline E2E final answer'), `deliverable carries planner final; got: ${deliverable.slice(-400)}`);
286
+ }
287
+ console.log(`agent turn deadline E2E: OK (exit=${exitCode}, plannerRequests=${relay.plannerBodies.length}, handoffResults=${handoffResults.length}, ticket=${ticketFiles[0]}, collected=settled)`);
288
+ await relay.close();
289
+ rmSync(dshHome, {
290
+ recursive: true,
291
+ force: true
292
+ });
293
+ rmSync(childCwd, {
294
+ recursive: true,
295
+ force: true
296
+ });
297
+ rmSync(handoffRoot, {
298
+ recursive: true,
299
+ force: true
300
+ });
301
+ }
302
+ await main();
303
+
304
+
305
+ //# sourceURL=ts/scripts/agent-planning-turn-deadline-e2e.ts
@@ -0,0 +1,281 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { installTurnDeadline, TURN_DEADLINE_EXHAUSTED, TURN_DEADLINE_HANDOFF, TURN_HANDOFF_SCHEMA, writeTurnHandoffTicket } from '../src/turn-deadline.js';
6
+ const success = ()=>({
7
+ isError: false,
8
+ value: null,
9
+ content: []
10
+ });
11
+ const handlers = new Map();
12
+ const restricted = [];
13
+ const lifted = [];
14
+ const makeAgent = (id)=>({
15
+ id,
16
+ ctx: {
17
+ tools: {
18
+ restrict (filter) {
19
+ restricted.push({
20
+ agentId: id,
21
+ deny: [
22
+ ...filter.deny
23
+ ]
24
+ });
25
+ return ()=>lifted.push(id);
26
+ }
27
+ }
28
+ }
29
+ });
30
+ const ctx = {
31
+ tools: {
32
+ schemas: ()=>[
33
+ {
34
+ name: 'gotry_alpha'
35
+ },
36
+ {
37
+ name: 'gotry_beta'
38
+ }
39
+ ]
40
+ },
41
+ on (event, listener) {
42
+ const bucket = handlers.get(event) ?? [];
43
+ bucket.push(listener);
44
+ handlers.set(event, bucket);
45
+ return ()=>{};
46
+ }
47
+ };
48
+ const productCtx = {
49
+ tools: {
50
+ schemas: ()=>[]
51
+ },
52
+ on (event) {
53
+ throw new Error(`opt-out path must not subscribe to ${event}`);
54
+ }
55
+ };
56
+ installTurnDeadline(productCtx, {
57
+ enabled: false
58
+ });
59
+ const clock = (()=>{
60
+ let t = 1_000_000;
61
+ return {
62
+ now: ()=>t,
63
+ advance: (ms)=>{
64
+ t += ms;
65
+ }
66
+ };
67
+ })();
68
+ const handoffRoot = mkdtempSync(join(tmpdir(), 'gotry-turn-handoff-tests-'));
69
+ installTurnDeadline(ctx, {
70
+ stateRoot: handoffRoot,
71
+ now: clock.now
72
+ });
73
+ const execute = handlers.get('tools/execute')?.[0];
74
+ const postExecute = handlers.get('tools/post-execute')?.[0];
75
+ const sessionEvent = handlers.get('session/event')?.[0];
76
+ const sessionDisposed = handlers.get('session/disposed')?.[0];
77
+ assert.ok(execute && postExecute && sessionEvent && sessionDisposed, 'all Cordis boundary hooks register');
78
+ const userMessage = (id, text, kind = 'user')=>sessionEvent({
79
+ id
80
+ }, {
81
+ type: 'user/message',
82
+ data: {
83
+ source: {
84
+ kind
85
+ },
86
+ content: [
87
+ {
88
+ type: 'text',
89
+ text
90
+ }
91
+ ]
92
+ }
93
+ });
94
+ const turnEvent = (id, type)=>sessionEvent({
95
+ id
96
+ }, {
97
+ type
98
+ });
99
+ {
100
+ let directBodies = 0;
101
+ for(let i = 0; i < 5; i++){
102
+ const result = await execute({
103
+ token: Symbol(`direct-${i}`)
104
+ }, async ()=>{
105
+ directBodies++;
106
+ return success();
107
+ });
108
+ assert.equal(result.isError, false);
109
+ }
110
+ assert.equal(directBodies, 5);
111
+ }{
112
+ const agent = makeAgent('agent-converge');
113
+ turnEvent(agent.id, 'turn/start');
114
+ userMessage(agent.id, '帮我看看10.1去杭州的高铁票');
115
+ {
116
+ const exec = {
117
+ token: Symbol('conv-1'),
118
+ agent
119
+ };
120
+ const result = await execute(exec, async ()=>success());
121
+ const post = await postExecute(exec, result, async ()=>({
122
+ kind: 'accept'
123
+ }));
124
+ assert.equal(result.isError, false);
125
+ assert.equal((post.additionalContexts ?? []).length, 0, 'no convergence context before soft');
126
+ assert.equal(restricted.length, 0);
127
+ }
128
+ clock.advance(300_001);
129
+ {
130
+ const exec = {
131
+ token: Symbol('conv-2'),
132
+ agent
133
+ };
134
+ const result = await execute(exec, async ()=>success());
135
+ const post = await postExecute(exec, result, async ()=>({
136
+ kind: 'accept'
137
+ }));
138
+ const contextText = post.additionalContexts?.flatMap((item)=>item.content ?? []).map((item)=>item.text ?? '').join('\n') ?? '';
139
+ assert.ok(contextText.includes('TURN_DEADLINE_SOFT'), 'soft context after crossing softMs');
140
+ assert.ok(contextText.includes('final answer'), 'converge soft text asks for a final answer');
141
+ }
142
+ clock.advance(300_000);
143
+ {
144
+ const exec = {
145
+ token: Symbol('conv-3'),
146
+ agent
147
+ };
148
+ let bodyRan = false;
149
+ const result = await execute(exec, async ()=>{
150
+ bodyRan = true;
151
+ return success();
152
+ });
153
+ assert.equal(bodyRan, false);
154
+ assert.equal(result.isError, true);
155
+ assert.deepEqual(result.error?.info, {
156
+ name: 'TurnDeadlineError',
157
+ code: TURN_DEADLINE_EXHAUSTED
158
+ });
159
+ assert.match(result.content[0]?.text ?? '', /TURN_DEADLINE_EXHAUSTED/);
160
+ }
161
+ assert.deepEqual(restricted.at(-1), {
162
+ agentId: agent.id,
163
+ deny: [
164
+ 'gotry_alpha',
165
+ 'gotry_beta'
166
+ ]
167
+ });
168
+ const convergeTicketDir = join(handoffRoot, 'gotry-state', 'turn-handoffs');
169
+ assert.equal(existsSync(convergeTicketDir) ? readdirSync(convergeTicketDir).length : 0, 0, 'converge writes no ticket');
170
+ turnEvent(agent.id, 'turn/end');
171
+ assert.equal(lifted.at(-1), agent.id);
172
+ }{
173
+ const agent = makeAgent('agent-handoff');
174
+ turnEvent(agent.id, 'turn/start');
175
+ const deepMessage = '2026年我还有6天IRW额度 ,我准备再请几天假,在国内待个十几天。' + '其中10.3要去湖南衡阳参加同学婚礼。其他我没有特别的要求,请根据我的实际情况,偏好,外部环境等因素,给我安排行程';
176
+ userMessage(agent.id, deepMessage);
177
+ clock.advance(120_001);
178
+ {
179
+ const exec = {
180
+ token: Symbol('hand-1'),
181
+ agent
182
+ };
183
+ const result = await execute(exec, async ()=>success());
184
+ const post = await postExecute(exec, result, async ()=>({
185
+ kind: 'accept'
186
+ }));
187
+ const contextText = post.additionalContexts?.flatMap((item)=>item.content ?? []).map((item)=>item.text ?? '').join('\n') ?? '';
188
+ assert.ok(contextText.includes('background planning'), 'handoff soft text announces the handoff');
189
+ assert.equal(result.isError, false);
190
+ }
191
+ clock.advance(120_000);
192
+ {
193
+ const exec = {
194
+ token: Symbol('hand-2'),
195
+ agent
196
+ };
197
+ let bodyRan = false;
198
+ const result = await execute(exec, async ()=>{
199
+ bodyRan = true;
200
+ return success();
201
+ });
202
+ assert.equal(bodyRan, false);
203
+ assert.equal(result.isError, true);
204
+ assert.deepEqual(result.error?.info, {
205
+ name: 'TurnDeadlineHandoff',
206
+ code: TURN_DEADLINE_HANDOFF
207
+ });
208
+ const text = result.content[0]?.text ?? '';
209
+ assert.match(text, /TURN_DEADLINE_HANDOFF/);
210
+ assert.match(text, /th-/, 'result names the persisted ticket id');
211
+ assert.match(text, /约 1 小时/, 'ETA label mirrors the S5 promise');
212
+ }
213
+ assert.deepEqual(restricted.at(-1), {
214
+ agentId: agent.id,
215
+ deny: [
216
+ 'gotry_alpha',
217
+ 'gotry_beta'
218
+ ]
219
+ });
220
+ const tickets = readdirSync(join(handoffRoot, 'gotry-state', 'turn-handoffs'));
221
+ assert.equal(tickets.length, 1, 'exactly one handoff ticket');
222
+ const ticket = JSON.parse(readFileSync(join(handoffRoot, 'gotry-state', 'turn-handoffs', tickets[0]), 'utf8'));
223
+ assert.equal(ticket.schema, TURN_HANDOFF_SCHEMA);
224
+ assert.equal(ticket.status, 'open');
225
+ assert.equal(ticket.userMessage, deepMessage, 'ticket carries the verbatim user ask');
226
+ assert.ok(ticket.requestedAt);
227
+ assert.equal(ticket.etaLabel, '约 1 小时');
228
+ turnEvent(agent.id, 'turn/end');
229
+ userMessage(agent.id, '你好');
230
+ {
231
+ const result = await execute({
232
+ token: Symbol('hand-3'),
233
+ agent
234
+ }, async ()=>success());
235
+ assert.equal(result.isError, false);
236
+ }
237
+ turnEvent(agent.id, 'turn/end');
238
+ assert.equal(readdirSync(join(handoffRoot, 'gotry-state', 'turn-handoffs')).length, 1, 'no extra tickets');
239
+ }{
240
+ const agent = makeAgent('agent-plugin-msg');
241
+ turnEvent(agent.id, 'turn/start');
242
+ userMessage(agent.id, '你好');
243
+ userMessage(agent.id, deepPlanningNoise(), 'plugin');
244
+ clock.advance(1_000_000);
245
+ {
246
+ const exec = {
247
+ token: Symbol('plug-1'),
248
+ agent
249
+ };
250
+ const result = await execute(exec, async ()=>success());
251
+ assert.deepEqual(result.error?.info, {
252
+ name: 'TurnDeadlineError',
253
+ code: TURN_DEADLINE_EXHAUSTED
254
+ });
255
+ }
256
+ turnEvent(agent.id, 'turn/end');
257
+ }function deepPlanningNoise() {
258
+ return '系统提醒样例:10.3 婚礼 IRW 请假 十几天 安排行程——这是插件注入,不该参与路由';
259
+ }
260
+ {
261
+ const root = mkdtempSync(join(tmpdir(), 'gotry-turn-handoff-writer-'));
262
+ const ticket = await writeTurnHandoffTicket(root, '短消息');
263
+ const dir = join(root, 'gotry-state', 'turn-handoffs');
264
+ assert.equal(readdirSync(dir).length, 1);
265
+ const loaded = JSON.parse(readFileSync(join(dir, `${ticket.id}.json`), 'utf8'));
266
+ assert.equal(loaded.userMessage, '短消息');
267
+ rmSync(root, {
268
+ recursive: true,
269
+ force: true
270
+ });
271
+ }sessionDisposed({
272
+ id: 'agent-handoff'
273
+ });
274
+ rmSync(handoffRoot, {
275
+ recursive: true,
276
+ force: true
277
+ });
278
+ console.log('agent turn deadline tests: OK (routing→converge/handoff exits, ticket persistence, plugin-message discipline, lifecycle)');
279
+
280
+
281
+ //# sourceURL=ts/scripts/agent-planning-turn-deadline-tests.ts