@downcity/agent 1.1.64 → 1.1.66

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.
@@ -1,9 +1,10 @@
1
1
  /**
2
- * @file 验证 CityModel 适配层会把本地 tool result 带回下一轮请求。
2
+ * @file 验证 CityModel 会优先走 OpenAI-compatible LanguageModel 并完成 tool loop。
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - 这里直接走编译后的 Agent / City 产物,避免测试只覆盖源码级辅助函数。
6
- * - 重点锁住 CityModelAdapter tool-call -> 本地执行 -> tool-result 回传链路。
6
+ * - 重点锁住 CityModel -> LanguageModel -> tool-call -> 本地执行 -> tool-result 回传链路。
7
+ * - 新路径不应再调用 `/v1/ai/stream`,避免 UIMessage stream 反向适配丢失 finish 语义。
7
8
  */
8
9
 
9
10
  import test from "node:test";
@@ -17,15 +18,15 @@ import { City } from "../../city/bin/index.js";
17
18
  import { tool } from "ai";
18
19
  import { z } from "zod";
19
20
 
20
- function write_sse(res, chunks) {
21
+ function write_openai_sse(res, chunks) {
21
22
  res.writeHead(200, {
22
23
  "content-type": "text/event-stream; charset=utf-8",
23
24
  "cache-control": "no-cache",
24
25
  connection: "keep-alive",
25
- "x-vercel-ai-ui-message-stream": "v1",
26
26
  });
27
27
  for (const chunk of chunks) {
28
- res.write(`data: ${JSON.stringify(chunk)}\n\n`);
28
+ const payload = typeof chunk === "string" ? chunk : JSON.stringify(chunk);
29
+ res.write(`data: ${payload}\n\n`);
29
30
  }
30
31
  res.end();
31
32
  }
@@ -42,8 +43,10 @@ async function read_json_body(req) {
42
43
  return JSON.parse(String(raw || "{}"));
43
44
  }
44
45
 
45
- test("CityModel sends tool result back on the next round", async () => {
46
+ test("CityModel uses direct LanguageModel path and sends tool result back", async () => {
46
47
  const requests = [];
48
+ const agent_requests = [];
49
+ let stream_requests = 0;
47
50
  let tool_executed = false;
48
51
 
49
52
  const server = http.createServer(async (req, res) => {
@@ -68,30 +71,149 @@ test("CityModel sends tool result back on the next round", async () => {
68
71
  }
69
72
 
70
73
  if (req.method === "POST" && url.pathname === "/v1/ai/stream") {
74
+ stream_requests += 1;
75
+ res.writeHead(500, { "content-type": "application/json" });
76
+ res.end(JSON.stringify({ error: "legacy stream endpoint should not be called" }));
77
+ return;
78
+ }
79
+
80
+ if (req.method === "POST" && url.pathname === "/v1/ai/chat/completions") {
71
81
  const body = await read_json_body(req);
72
82
  requests.push(body);
73
83
 
74
- if (requests.length === 1) {
75
- write_sse(res, [
76
- { type: "start", messageId: "msg_1" },
77
- { type: "tool-input-start", toolCallId: "call_1", toolName: "ping" },
84
+ if (!Array.isArray(body.tools)) {
85
+ write_openai_sse(res, [
86
+ {
87
+ id: "chatcmpl_title",
88
+ object: "chat.completion.chunk",
89
+ created: 1,
90
+ model: "mock-model",
91
+ choices: [
92
+ {
93
+ index: 0,
94
+ delta: { role: "assistant", content: "Tool loop" },
95
+ finish_reason: null,
96
+ },
97
+ ],
98
+ },
99
+ {
100
+ id: "chatcmpl_title",
101
+ object: "chat.completion.chunk",
102
+ created: 1,
103
+ model: "mock-model",
104
+ choices: [
105
+ {
106
+ index: 0,
107
+ delta: {},
108
+ finish_reason: "stop",
109
+ },
110
+ ],
111
+ },
112
+ "[DONE]",
113
+ ]);
114
+ return;
115
+ }
116
+
117
+ agent_requests.push(body);
118
+ if (agent_requests.length === 1) {
119
+ write_openai_sse(res, [
120
+ {
121
+ id: "chatcmpl_1",
122
+ object: "chat.completion.chunk",
123
+ created: 1,
124
+ model: "mock-model",
125
+ choices: [
126
+ {
127
+ index: 0,
128
+ delta: { role: "assistant" },
129
+ finish_reason: null,
130
+ },
131
+ ],
132
+ },
133
+ {
134
+ id: "chatcmpl_1",
135
+ object: "chat.completion.chunk",
136
+ created: 1,
137
+ model: "mock-model",
138
+ choices: [
139
+ {
140
+ index: 0,
141
+ delta: {
142
+ tool_calls: [
143
+ {
144
+ index: 0,
145
+ id: "call_1",
146
+ type: "function",
147
+ function: {
148
+ name: "ping",
149
+ arguments: "{\"value\":\"hello\"}",
150
+ },
151
+ },
152
+ ],
153
+ },
154
+ finish_reason: null,
155
+ },
156
+ ],
157
+ },
78
158
  {
79
- type: "tool-input-available",
80
- toolCallId: "call_1",
81
- toolName: "ping",
82
- input: { value: "hello" },
159
+ id: "chatcmpl_1",
160
+ object: "chat.completion.chunk",
161
+ created: 1,
162
+ model: "mock-model",
163
+ choices: [
164
+ {
165
+ index: 0,
166
+ delta: {},
167
+ finish_reason: "tool_calls",
168
+ },
169
+ ],
83
170
  },
84
- { type: "finish", finishReason: "tool-calls" },
171
+ "[DONE]",
85
172
  ]);
86
173
  return;
87
174
  }
88
175
 
89
- write_sse(res, [
90
- { type: "start", messageId: "msg_2" },
91
- { type: "text-start", id: "text_2" },
92
- { type: "text-delta", id: "text_2", delta: "done" },
93
- { type: "text-end", id: "text_2" },
94
- { type: "finish", finishReason: "stop" },
176
+ write_openai_sse(res, [
177
+ {
178
+ id: "chatcmpl_2",
179
+ object: "chat.completion.chunk",
180
+ created: 1,
181
+ model: "mock-model",
182
+ choices: [
183
+ {
184
+ index: 0,
185
+ delta: { role: "assistant" },
186
+ finish_reason: null,
187
+ },
188
+ ],
189
+ },
190
+ {
191
+ id: "chatcmpl_2",
192
+ object: "chat.completion.chunk",
193
+ created: 1,
194
+ model: "mock-model",
195
+ choices: [
196
+ {
197
+ index: 0,
198
+ delta: { content: "done" },
199
+ finish_reason: null,
200
+ },
201
+ ],
202
+ },
203
+ {
204
+ id: "chatcmpl_2",
205
+ object: "chat.completion.chunk",
206
+ created: 1,
207
+ model: "mock-model",
208
+ choices: [
209
+ {
210
+ index: 0,
211
+ delta: {},
212
+ finish_reason: "stop",
213
+ },
214
+ ],
215
+ },
216
+ "[DONE]",
95
217
  ]);
96
218
  return;
97
219
  }
@@ -142,31 +264,19 @@ test("CityModel sends tool result back on the next round", async () => {
142
264
 
143
265
  assert.equal(result.success, true);
144
266
  assert.equal(tool_executed, true);
145
- assert.equal(requests.length, 2);
267
+ assert.equal(stream_requests, 0);
268
+ assert.equal(agent_requests.length, 2);
269
+ assert.equal(requests.every((request) => request?.town_id === "town_demo"), true);
270
+ assert.equal(agent_requests[0]?.model, "mock-model");
146
271
 
147
- const second_request_messages = Array.isArray(requests[1]?.messages)
148
- ? requests[1].messages
272
+ const second_request_messages = Array.isArray(agent_requests[1]?.messages)
273
+ ? agent_requests[1].messages
149
274
  : [];
150
- const second_assistant_message = second_request_messages.find(
151
- (message) => message && message.role === "assistant",
152
- );
153
- assert.ok(second_assistant_message);
154
- const second_assistant_parts = Array.isArray(second_assistant_message.parts)
155
- ? second_assistant_message.parts
156
- : [];
157
-
158
- const tool_call_part = second_assistant_parts.find(
159
- (part) => part && part.type === "dynamic-tool" && part.state === "output-available",
160
- );
161
- assert.deepEqual(tool_call_part, {
162
- type: "dynamic-tool",
163
- toolName: "ping",
164
- toolCallId: "call_1",
165
- state: "output-available",
166
- input: { value: "hello" },
167
- output: { echoed: "hello" },
168
- providerExecuted: false,
169
- });
275
+ const serialized_second_messages = JSON.stringify(second_request_messages);
276
+ assert.match(serialized_second_messages, /"role":"tool"/);
277
+ assert.match(serialized_second_messages, /call_1/);
278
+ assert.match(serialized_second_messages, /echoed/);
279
+ assert.match(serialized_second_messages, /hello/);
170
280
  } finally {
171
281
  await new Promise((resolve, reject) => {
172
282
  server.close((error) => {