@downcity/agent 1.1.259 → 1.1.261

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.
@@ -3,16 +3,14 @@
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - Agent 与 Session 对外统一接收 AgentModel,executor 内部只处理 AI SDK LanguageModel。
6
- * - CityModel 保留模型目录信息;运行时连接信息通过隐藏协议提供。
7
- * - 这里直接创建 OpenAI-compatible LanguageModel,不再保留旧的 text/stream 反向适配。
6
+ * - CityModel 自身实现 LanguageModelV3,executor 可以直接调用。
8
7
  * - 这里不依赖 @downcity/city,只依赖 @downcity/type 的共享协议。
9
8
  */
10
- import { type CityModel } from "@downcity/type";
11
9
  import type { LanguageModel } from "ai";
12
10
  /**
13
11
  * Agent SDK 可接受的模型输入。
14
12
  */
15
- export type AgentModel = LanguageModel | CityModel;
13
+ export type AgentModel = LanguageModel;
16
14
  /**
17
15
  * 将 Agent 可接受的模型输入归一为 AI SDK LanguageModel。
18
16
  */
@@ -1 +1 @@
1
- {"version":3,"file":"AgentModel.d.ts","sourceRoot":"","sources":["../../src/agent/AgentModel.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAGL,KAAK,SAAS,EACf,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAKxC;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,GAAG,SAAS,CAAC;AAenD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa,CASpE;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,UAAU,GAAG,SAAS,GAC5B,MAAM,GAAG,SAAS,CAMpB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAqBtF"}
1
+ {"version":3,"file":"AgentModel.d.ts","sourceRoot":"","sources":["../../src/agent/AgentModel.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,IAAI,CAAC;AAExC;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,aAAa,CAAC;AAEvC;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,GAAG,aAAa,CAEpE;AAED;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,UAAU,GAAG,SAAS,GAC5B,MAAM,GAAG,SAAS,CAMpB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAqBtF"}
@@ -3,38 +3,14 @@
3
3
  *
4
4
  * 关键点(中文)
5
5
  * - Agent 与 Session 对外统一接收 AgentModel,executor 内部只处理 AI SDK LanguageModel。
6
- * - CityModel 保留模型目录信息;运行时连接信息通过隐藏协议提供。
7
- * - 这里直接创建 OpenAI-compatible LanguageModel,不再保留旧的 text/stream 反向适配。
6
+ * - CityModel 自身实现 LanguageModelV3,executor 可以直接调用。
8
7
  * - 这里不依赖 @downcity/city,只依赖 @downcity/type 的共享协议。
9
8
  */
10
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
11
- import { CITY_MODEL_INVOKER, isCityModel, } from "@downcity/type";
12
- /** 已完成转换的 CityModel 与 LanguageModel 对应关系。 */
13
- const normalized_models = new WeakMap();
14
- /**
15
- * 将 CityModel 的 hidden connection 转换为 AI SDK LanguageModel。
16
- */
17
- function cityModelToLanguageModel(model) {
18
- const connection = model[CITY_MODEL_INVOKER].connection();
19
- const provider = createOpenAICompatible({
20
- name: "downcity",
21
- baseURL: connection.base_url,
22
- apiKey: connection.api_key,
23
- });
24
- return provider.languageModel(connection.model_id);
25
- }
9
+ import { isCityModel, } from "@downcity/type";
26
10
  /**
27
11
  * 将 Agent 可接受的模型输入归一为 AI SDK LanguageModel。
28
12
  */
29
13
  export function normalizeAgentModel(model) {
30
- if (isCityModel(model)) {
31
- const cached = normalized_models.get(model);
32
- if (cached)
33
- return cached;
34
- const normalized = cityModelToLanguageModel(model);
35
- normalized_models.set(model, normalized);
36
- return normalized;
37
- }
38
14
  return model;
39
15
  }
40
16
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"AgentModel.js","sourceRoot":"","sources":["../../src/agent/AgentModel.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EACL,kBAAkB,EAClB,WAAW,GAEZ,MAAM,gBAAgB,CAAC;AAGxB,6CAA6C;AAC7C,MAAM,iBAAiB,GAAG,IAAI,OAAO,EAAyB,CAAC;AAO/D;;GAEG;AACH,SAAS,wBAAwB,CAAC,KAAgB;IAChD,MAAM,UAAU,GAAG,KAAK,CAAC,kBAAkB,CAAC,CAAC,UAAU,EAAE,CAAC;IAC1D,MAAM,QAAQ,GAAG,sBAAsB,CAAC;QACtC,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,UAAU,CAAC,QAAQ;QAC5B,MAAM,EAAE,UAAU,CAAC,OAAO;KAC3B,CAAC,CAAC;IACH,OAAO,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,QAAQ,CAAkB,CAAC;AACtE,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAiB;IACnD,IAAI,WAAW,CAAC,KAAK,CAAC,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAC5C,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAC1B,MAAM,UAAU,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;QACnD,iBAAiB,CAAC,GAAG,CAAC,KAAK,EAAE,UAAU,CAAC,CAAC;QACzC,OAAO,UAAU,CAAC;IACpB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,KAA6B;IAE7B,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACpD,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,CAAC;IAC5C,OAAO,MAAM,CAAC,aAAa,CAAC,cAAc,CAAC,IAAI,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC;QACvE,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC;QACxB,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAA6B;IAChE,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,IAAI,WAAW,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;IACtD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,MAAM,UAAU,GAAG;QACjB,MAAM,CAAC,OAAO;QACd,MAAM,CAAC,KAAK;QACZ,MAAM,CAAC,EAAE;QACT,MAAM,CAAC,IAAI;QACX,MAAM,CAAC,KAAK;KACb,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,MAAM,eAAe,GACnB,KAAK,CAAC,WAAW,IAAI,OAAO,KAAK,CAAC,WAAW,CAAC,IAAI,KAAK,QAAQ;QAC7D,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE;QAC/B,CAAC,CAAC,EAAE,CAAC;IACT,OAAO,eAAe,IAAI,kBAAkB,CAAC;AAC/C,CAAC"}
1
+ {"version":3,"file":"AgentModel.js","sourceRoot":"","sources":["../../src/agent/AgentModel.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EACL,WAAW,GACZ,MAAM,gBAAgB,CAAC;AAQxB;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAiB;IACnD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,+BAA+B,CAC7C,KAA6B;IAE7B,IAAI,CAAC,KAAK,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACpD,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,CAAC;IAC5C,OAAO,MAAM,CAAC,aAAa,CAAC,cAAc,CAAC,IAAI,MAAM,CAAC,cAAc,CAAC,GAAG,CAAC;QACvE,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC;QACxB,CAAC,CAAC,SAAS,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAA6B;IAChE,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,IAAI,WAAW,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,EAAE,CAAC;IACtD,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChD,MAAM,MAAM,GAAG,KAAgC,CAAC;IAChD,MAAM,UAAU,GAAG;QACjB,MAAM,CAAC,OAAO;QACd,MAAM,CAAC,KAAK;QACZ,MAAM,CAAC,EAAE;QACT,MAAM,CAAC,IAAI;QACX,MAAM,CAAC,KAAK;KACb,CAAC;IACF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,MAAM,KAAK,GAAG,OAAO,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpE,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;IAC1B,CAAC;IACD,MAAM,eAAe,GACnB,KAAK,CAAC,WAAW,IAAI,OAAO,KAAK,CAAC,WAAW,CAAC,IAAI,KAAK,QAAQ;QAC7D,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,EAAE;QAC/B,CAAC,CAAC,EAAE,CAAC;IACT,OAAO,eAAe,IAAI,kBAAkB,CAAC;AAC/C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@downcity/agent",
3
- "version": "1.1.259",
3
+ "version": "1.1.261",
4
4
  "type": "module",
5
5
  "description": "Downcity Agent 运行时 — 单 Agent 执行壳与本机 RPC 能力",
6
6
  "main": "./bin/index.js",
@@ -12,9 +12,8 @@
12
12
  }
13
13
  },
14
14
  "dependencies": {
15
- "@ai-sdk/openai-compatible": "^2.0.48",
16
- "@downcity/shell": "^0.1.113",
17
- "@downcity/type": "0.1.146",
15
+ "@downcity/shell": "^0.1.116",
16
+ "@downcity/type": "0.1.148",
18
17
  "ai": "^6.0.193",
19
18
  "commander": "^15.0.0",
20
19
  "dotenv": "^17.4.2",
@@ -1,309 +1,172 @@
1
1
  /**
2
- * @file 验证 CityModel 会优先走 OpenAI-compatible LanguageModel 并完成 tool loop。
2
+ * @file 验证 Agent 直接调用实现 LanguageModelV3 CityModel 并完成 tool loop。
3
3
  *
4
4
  * 关键点(中文)
5
- * - 这里直接走编译后的 Agent 产物,避免测试只覆盖源码级辅助函数。
6
- * - CityModel 使用 @downcity/type 的共享协议构造,避免反向依赖 City SDK 实现。
7
- * - 重点锁住 CityModel -> LanguageModel -> tool-call -> 本地执行 -> tool-result 回传链路。
8
- * - 新路径不应再调用 `/v1/ai/stream`,避免 UIMessage stream 反向适配丢失 finish 语义。
5
+ * - 测试模型自身已经实现 LanguageModelV3,Agent 不再创建第二个 Provider 模型。
6
+ * - 第一次调用返回 tool-call,Agent 本地执行后把 tool-result 放进第二次调用。
7
+ * - CityModel 的目录信息继续用于上下文窗口和日志,不参与网络连接转换。
9
8
  */
10
9
 
11
10
  import test from "node:test";
12
11
  import assert from "node:assert/strict";
13
- import http from "node:http";
14
12
  import os from "node:os";
15
13
  import path from "node:path";
16
14
  import fs from "node:fs/promises";
17
- import { Agent } from "../bin/index.js";
18
- import { createAction, createPlugin } from "../bin/plugin/core/PluginActionFactory.js";
19
- import { CITY_MODEL_INVOKER, CITY_MODEL_KIND } from "@downcity/type";
15
+ import { MockLanguageModelV3 } from "ai/test";
20
16
  import { tool } from "ai";
21
17
  import { z } from "zod";
22
18
 
23
- function write_openai_sse(res, chunks) {
24
- res.writeHead(200, {
25
- "content-type": "text/event-stream; charset=utf-8",
26
- "cache-control": "no-cache",
27
- connection: "keep-alive",
28
- });
29
- for (const chunk of chunks) {
30
- const payload = typeof chunk === "string" ? chunk : JSON.stringify(chunk);
31
- res.write(`data: ${payload}\n\n`);
32
- }
33
- res.end();
19
+ import { Agent } from "../bin/index.js";
20
+ import { createAction, createPlugin } from "../bin/plugin/core/PluginActionFactory.js";
21
+ import { CITY_MODEL_KIND } from "@downcity/type";
22
+
23
+ /** 构造 AI SDK V3 usage。 */
24
+ function create_usage() {
25
+ return {
26
+ inputTokens: { total: 5, noCache: 5, cacheRead: 0, cacheWrite: 0 },
27
+ outputTokens: { total: 3, text: 3, reasoning: 0 },
28
+ };
34
29
  }
35
30
 
36
- async function read_json_body(req) {
37
- const raw = await new Promise((resolve, reject) => {
38
- let data = "";
39
- req.on("data", (chunk) => {
40
- data += chunk;
41
- });
42
- req.on("end", () => resolve(data));
43
- req.on("error", reject);
44
- });
45
- return JSON.parse(String(raw || "{}"));
31
+ /** 构造一次 ping tool-call。 */
32
+ function create_tool_call_stream() {
33
+ const input = JSON.stringify({ value: "hello" });
34
+ return {
35
+ stream: new ReadableStream({
36
+ start(controller) {
37
+ controller.enqueue({ type: "stream-start", warnings: [] });
38
+ controller.enqueue({ type: "tool-input-start", id: "call_1", toolName: "ping" });
39
+ controller.enqueue({ type: "tool-input-delta", id: "call_1", delta: input });
40
+ controller.enqueue({ type: "tool-input-end", id: "call_1" });
41
+ controller.enqueue({
42
+ type: "tool-call",
43
+ toolCallId: "call_1",
44
+ toolName: "ping",
45
+ input,
46
+ });
47
+ controller.enqueue({
48
+ type: "finish",
49
+ finishReason: { unified: "tool-calls", raw: "tool_calls" },
50
+ usage: create_usage(),
51
+ });
52
+ controller.close();
53
+ },
54
+ }),
55
+ };
46
56
  }
47
57
 
48
- test("CityModel uses direct LanguageModel path and sends tool result back", async () => {
49
- const agent_requests = [];
50
- let stream_requests = 0;
51
- let tool_executed = false;
52
-
53
- const server = http.createServer(async (req, res) => {
54
- const url = new URL(String(req.url || "/"), "http://127.0.0.1");
55
-
56
- if (req.method === "POST" && url.pathname === "/v1/ai/stream") {
57
- stream_requests += 1;
58
- res.writeHead(500, { "content-type": "application/json" });
59
- res.end(JSON.stringify({ error: "legacy stream endpoint should not be called" }));
60
- return;
61
- }
62
-
63
- if (req.method === "POST" && url.pathname === "/v1/ai/chat/completions") {
64
- const body = await read_json_body(req);
65
-
66
- if (!Array.isArray(body.tools)) {
67
- write_openai_sse(res, [
68
- {
69
- id: "chatcmpl_title",
70
- object: "chat.completion.chunk",
71
- created: 1,
72
- model: "mock-model",
73
- choices: [
74
- {
75
- index: 0,
76
- delta: { role: "assistant", content: "Tool loop" },
77
- finish_reason: null,
78
- },
79
- ],
80
- },
81
- {
82
- id: "chatcmpl_title",
83
- object: "chat.completion.chunk",
84
- created: 1,
85
- model: "mock-model",
86
- choices: [
87
- {
88
- index: 0,
89
- delta: {},
90
- finish_reason: "stop",
91
- },
92
- ],
93
- },
94
- "[DONE]",
95
- ]);
96
- return;
97
- }
58
+ /** 构造普通文本完成流。 */
59
+ function create_text_stream(text) {
60
+ return {
61
+ stream: new ReadableStream({
62
+ start(controller) {
63
+ controller.enqueue({ type: "stream-start", warnings: [] });
64
+ controller.enqueue({ type: "text-start", id: "text_1" });
65
+ controller.enqueue({ type: "text-delta", id: "text_1", delta: text });
66
+ controller.enqueue({ type: "text-end", id: "text_1" });
67
+ controller.enqueue({
68
+ type: "finish",
69
+ finishReason: { unified: "stop", raw: "stop" },
70
+ usage: create_usage(),
71
+ });
72
+ controller.close();
73
+ },
74
+ }),
75
+ };
76
+ }
98
77
 
99
- agent_requests.push(body);
100
- if (agent_requests.length === 1) {
101
- write_openai_sse(res, [
102
- {
103
- id: "chatcmpl_1",
104
- object: "chat.completion.chunk",
105
- created: 1,
106
- model: "mock-model",
107
- choices: [
108
- {
109
- index: 0,
110
- delta: { role: "assistant" },
111
- finish_reason: null,
112
- },
113
- ],
114
- },
115
- {
116
- id: "chatcmpl_1",
117
- object: "chat.completion.chunk",
118
- created: 1,
119
- model: "mock-model",
120
- choices: [
121
- {
122
- index: 0,
123
- delta: {
124
- tool_calls: [
125
- {
126
- index: 0,
127
- id: "call_1",
128
- type: "function",
129
- function: {
130
- name: "ping",
131
- arguments: "{\"value\":\"hello\"}",
132
- },
133
- },
134
- ],
135
- },
136
- finish_reason: null,
137
- },
138
- ],
139
- },
140
- {
141
- id: "chatcmpl_1",
142
- object: "chat.completion.chunk",
143
- created: 1,
144
- model: "mock-model",
145
- choices: [
146
- {
147
- index: 0,
148
- delta: {},
149
- finish_reason: "tool_calls",
150
- },
151
- ],
152
- },
153
- "[DONE]",
154
- ]);
155
- return;
78
+ /** 给标准 LanguageModelV3 附加 CityModel 目录协议。 */
79
+ function create_city_model(model_requests) {
80
+ let request_count = 0;
81
+ const model = new MockLanguageModelV3({
82
+ modelId: "mock-model",
83
+ doStream: async (options) => {
84
+ if (!Array.isArray(options.tools) || options.tools.length === 0) {
85
+ return create_text_stream("Tool loop");
156
86
  }
87
+ request_count += 1;
88
+ model_requests.push(options);
89
+ return request_count === 1
90
+ ? create_tool_call_stream()
91
+ : create_text_stream("done");
92
+ },
93
+ });
94
+ return Object.assign(model, {
95
+ id: "mock-model",
96
+ name: "Mock Model",
97
+ description: "Native CityModel tool loop test",
98
+ modalities: ["text", "stream"],
99
+ tags: [],
100
+ meta: {},
101
+ kind: CITY_MODEL_KIND,
102
+ });
103
+ }
157
104
 
158
- write_openai_sse(res, [
159
- {
160
- id: "chatcmpl_2",
161
- object: "chat.completion.chunk",
162
- created: 1,
163
- model: "mock-model",
164
- choices: [
165
- {
166
- index: 0,
167
- delta: { role: "assistant" },
168
- finish_reason: null,
169
- },
170
- ],
171
- },
172
- {
173
- id: "chatcmpl_2",
174
- object: "chat.completion.chunk",
175
- created: 1,
176
- model: "mock-model",
177
- choices: [
178
- {
179
- index: 0,
180
- delta: { content: "done" },
181
- finish_reason: null,
182
- },
183
- ],
184
- },
185
- {
186
- id: "chatcmpl_2",
187
- object: "chat.completion.chunk",
188
- created: 1,
189
- model: "mock-model",
190
- choices: [
191
- {
192
- index: 0,
193
- delta: {},
194
- finish_reason: "stop",
195
- },
196
- ],
105
+ test("CityModel uses direct LanguageModel path and sends tool result back", async () => {
106
+ const model_requests = [];
107
+ let tool_executed = false;
108
+ const agent_path = await fs.mkdtemp(
109
+ path.join(os.tmpdir(), "downcity-agent-city-model-tool-loop-"),
110
+ );
111
+ const skill_plugin = createPlugin({
112
+ name: "skill",
113
+ title: "Skill",
114
+ description: "Test skill plugin",
115
+ actions: {
116
+ lookup: createAction({
117
+ description: "Lookup a skill",
118
+ execute: async ({ input }) => ({
119
+ success: true,
120
+ data: { name: input.name },
121
+ message: "loaded",
122
+ }),
123
+ }),
124
+ },
125
+ });
126
+ const agent = new Agent({
127
+ id: "tool_loop_agent",
128
+ path: agent_path,
129
+ plugins: [skill_plugin],
130
+ tools: {
131
+ ping: tool({
132
+ description: "ping tool",
133
+ inputSchema: z.object({ value: z.string() }),
134
+ execute: async ({ value }, options) => {
135
+ tool_executed = true;
136
+ options.experimental_context.session_run_context.pendingAssistantFileParts.push({
137
+ type: "file",
138
+ mediaType: "image/png",
139
+ url: ".downcity/resources/tool-output.png",
140
+ filename: "tool-output.png",
141
+ });
142
+ return { echoed: value };
197
143
  },
198
- "[DONE]",
199
- ]);
200
- return;
201
- }
202
-
203
- res.writeHead(404);
204
- res.end("not found");
144
+ }),
145
+ },
205
146
  });
206
147
 
207
- await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
208
-
209
- let agent;
210
148
  try {
211
- const address = server.address();
212
- assert.ok(address && typeof address === "object");
213
- const model = Object.freeze({
214
- id: "mock-model",
215
- name: "Mock Model",
216
- description: "mock",
217
- modalities: ["text", "stream"],
218
- tags: [],
219
- meta: {},
220
- kind: CITY_MODEL_KIND,
221
- [CITY_MODEL_INVOKER]: {
222
- connection: () => ({
223
- base_url: `http://127.0.0.1:${String(address.port)}/v1/ai`,
224
- api_key: "ub_test",
225
- model_id: "mock-model",
226
- }),
227
- },
228
- });
229
-
230
- const agent_path = await fs.mkdtemp(
231
- path.join(os.tmpdir(), "downcity-agent-city-model-tool-loop-"),
232
- );
233
- const skill_plugin = createPlugin({
234
- name: "skill",
235
- title: "Skill",
236
- description: "Test skill plugin",
237
- actions: {
238
- lookup: createAction({
239
- description: "Lookup a skill",
240
- execute: async ({ input }) => ({
241
- success: true,
242
- data: { name: input.name },
243
- message: "loaded",
244
- }),
245
- }),
246
- },
247
- });
248
- agent = new Agent({
249
- id: "tool_loop_agent",
250
- path: agent_path,
251
- plugins: [skill_plugin],
252
- tools: {
253
- ping: tool({
254
- description: "ping tool",
255
- inputSchema: z.object({
256
- value: z.string(),
257
- }),
258
- execute: async ({ value }, options) => {
259
- tool_executed = true;
260
- options.experimental_context.session_run_context.pendingAssistantFileParts.push({
261
- type: "file",
262
- mediaType: "image/png",
263
- url: ".downcity/resources/tool-output.png",
264
- filename: "tool-output.png",
265
- });
266
- return { echoed: value };
267
- },
268
- }),
269
- },
270
- });
271
-
272
149
  const session = await agent.sessions.create();
273
- await session.set({ model });
150
+ await session.set({ model: create_city_model(model_requests) });
274
151
  const turn = await session.prompt({ query: "please use the ping tool" });
275
152
  const result = await turn.finished;
276
153
 
277
154
  assert.equal(result.success, true);
278
155
  assert.equal(tool_executed, true);
279
- assert.equal(stream_requests, 0);
280
- assert.equal(agent_requests.length, 2);
281
- assert.equal(agent_requests[0]?.model, "mock-model");
282
- const plugin_call_tool = agent_requests[0]?.tools?.find(
283
- (item) => item?.type === "function" && item?.function?.name === "plugin_call",
284
- );
156
+ assert.equal(model_requests.length, 2);
157
+ const plugin_call_tool = model_requests[0].tools.find((item) => item.name === "plugin_call");
285
158
  assert.ok(plugin_call_tool);
286
- const plugin_call_parameters = plugin_call_tool.function.parameters;
287
- assert.equal(plugin_call_parameters.type, "object");
288
- assert.deepEqual(plugin_call_parameters.required, ["plugin", "action"]);
289
- assert.equal(plugin_call_parameters.additionalProperties, false);
290
- assert.equal(
291
- plugin_call_parameters.properties.payload.additionalProperties,
292
- true,
293
- );
159
+ assert.equal(plugin_call_tool.inputSchema.type, "object");
160
+ assert.deepEqual(plugin_call_tool.inputSchema.required, ["plugin", "action"]);
161
+ assert.equal(plugin_call_tool.inputSchema.additionalProperties, false);
294
162
 
295
- const second_request_messages = Array.isArray(agent_requests[1]?.messages)
296
- ? agent_requests[1].messages
297
- : [];
298
- const serialized_second_messages = JSON.stringify(second_request_messages);
299
- assert.match(serialized_second_messages, /"role":"tool"/);
300
- assert.match(serialized_second_messages, /call_1/);
301
- assert.match(serialized_second_messages, /echoed/);
302
- assert.match(serialized_second_messages, /hello/);
163
+ const serialized_second_prompt = JSON.stringify(model_requests[1].prompt);
164
+ assert.match(serialized_second_prompt, /"role":"tool"/);
165
+ assert.match(serialized_second_prompt, /call_1/);
166
+ assert.match(serialized_second_prompt, /echoed/);
167
+ assert.match(serialized_second_prompt, /hello/);
303
168
 
304
- const result_file = result.assistantMessage.parts.find(
305
- (part) => part.type === "file",
306
- );
169
+ const result_file = result.assistantMessage.parts.find((part) => part.type === "file");
307
170
  assert.deepEqual(result_file, {
308
171
  type: "file",
309
172
  mediaType: "image/png",
@@ -311,32 +174,12 @@ test("CityModel uses direct LanguageModel path and sends tool result back", asyn
311
174
  filename: "tool-output.png",
312
175
  });
313
176
  const session_messages = await session.messages({ include_internal: true });
314
- const persisted_assistant = session_messages.items.find(
315
- (message) => message.type === "assistant",
316
- );
317
- const persisted_file = persisted_assistant.parts.find(
318
- (part) => part.type === "file",
319
- );
320
- assert.deepEqual(persisted_file, {
321
- part_id: persisted_file.part_id,
322
- sequence: persisted_file.sequence,
323
- type: "file",
324
- media_type: "image/png",
325
- url: ".downcity/resources/tool-output.png",
326
- filename: "tool-output.png",
327
- });
177
+ const persisted_assistant = session_messages.items.find((message) => message.type === "assistant");
178
+ const persisted_file = persisted_assistant.parts.find((part) => part.type === "file");
179
+ assert.equal(persisted_file.media_type, "image/png");
180
+ assert.equal(persisted_file.url, ".downcity/resources/tool-output.png");
181
+ assert.equal(persisted_file.filename, "tool-output.png");
328
182
  } finally {
329
- if (agent) {
330
- await agent.dispose();
331
- }
332
- await new Promise((resolve, reject) => {
333
- server.close((error) => {
334
- if (error) {
335
- reject(error);
336
- return;
337
- }
338
- resolve();
339
- });
340
- });
183
+ await agent.dispose();
341
184
  }
342
185
  });