@langwatch/mcp-server 2.0.0 → 2.1.0

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 (177) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +31 -3
  3. package/dist/{archive-project-4EQGLBJJ.js → archive-project-W3IKI7UL.js} +3 -3
  4. package/dist/archive-run-plan-474OKHHU.js +16 -0
  5. package/dist/{archive-scenario-W6BXYDZ3.js → archive-scenario-V5UNYK26.js} +3 -3
  6. package/dist/archive-test-suite-O7FLDMXK.js +16 -0
  7. package/dist/{assign-prompt-tag-VLXUWEKU.js → assign-prompt-tag-Q46WRB6P.js} +2 -2
  8. package/dist/chunk-56OGTS24.js +44 -0
  9. package/dist/chunk-6DJDEXET.js +63 -0
  10. package/dist/chunk-7E4FZIYJ.js +15 -0
  11. package/dist/{chunk-77BNWQ3U.js → chunk-AN4OUA4D.js} +2 -2
  12. package/dist/chunk-CKNAH5TG.js +74 -0
  13. package/dist/{chunk-TOXJQDIZ.js → chunk-EYXMLE5X.js} +85 -10
  14. package/dist/{chunk-7RHG56OH.js → chunk-G4ZAZTZW.js} +2 -2
  15. package/dist/{chunk-A2LEVYQ5.js → chunk-GELJ2MQR.js} +11 -2
  16. package/dist/chunk-HV4DWKBI.js +46 -0
  17. package/dist/chunk-JR6DJZOU.js +49 -0
  18. package/dist/{chunk-SFB4MU6H.js → chunk-JTQJRNFJ.js} +2 -2
  19. package/dist/{chunk-TJCXFNDT.js → chunk-K37ZNPXK.js} +2 -2
  20. package/dist/{chunk-CDYQOY5U.js → chunk-OJLYYJWI.js} +2 -2
  21. package/dist/{chunk-ZWIL4EFL.js → chunk-QAMP3CGR.js} +2 -2
  22. package/dist/{chunk-4QC7SA2L.js → chunk-QRO35SOM.js} +345 -144
  23. package/dist/{chunk-J42FWRJ2.js → chunk-RVAE2MEA.js} +2 -2
  24. package/dist/{chunk-AQJO2EI5.js → chunk-S4GVX36W.js} +2 -2
  25. package/dist/{chunk-YX3A3CFM.js → chunk-SR6QPRIE.js} +2 -2
  26. package/dist/chunk-UF6XIGYR.js +58 -0
  27. package/dist/{chunk-5CCZFUXS.js → chunk-UFOYFYI7.js} +2 -2
  28. package/dist/{create-agent-7XHO6UIS.js → create-agent-JFJ2K6X3.js} +3 -3
  29. package/dist/{create-annotation-YQXRQC6A.js → create-annotation-EUXXQ6NS.js} +3 -3
  30. package/dist/{create-api-key-YUS36KBU.js → create-api-key-BGBB76JX.js} +3 -3
  31. package/dist/{create-dashboard-4Z5JO5LB.js → create-dashboard-Z76AIPLS.js} +3 -3
  32. package/dist/{create-dataset-E6NMSVX7.js → create-dataset-FD5VAXU4.js} +3 -3
  33. package/dist/{create-dataset-records-SKHPRD2K.js → create-dataset-records-WQUXXOHI.js} +3 -3
  34. package/dist/{create-evaluator-JXF5PF33.js → create-evaluator-6NV33Z52.js} +3 -3
  35. package/dist/create-mcp-server.js +7 -2
  36. package/dist/{create-project-CS2CB57Y.js → create-project-ZQ7TFIO2.js} +3 -3
  37. package/dist/{create-prompt-F5RMX3L3.js → create-prompt-BS6QRCUU.js} +2 -2
  38. package/dist/{create-prompt-tag-GMRWTEFR.js → create-prompt-tag-FCQ5ELSQ.js} +2 -2
  39. package/dist/{create-scenario-JDR2VB2H.js → create-scenario-DRPTPXU3.js} +10 -3
  40. package/dist/create-test-suite-ZIZCCFJ4.js +37 -0
  41. package/dist/{delete-agent-O3EEH4S5.js → delete-agent-SA4JMUZF.js} +3 -3
  42. package/dist/{delete-annotation-XP6FQM7A.js → delete-annotation-X3KKK7QY.js} +3 -3
  43. package/dist/{delete-dashboard-RFUNYCNP.js → delete-dashboard-LYMRPF27.js} +3 -3
  44. package/dist/{delete-dataset-3MJFRDLP.js → delete-dataset-W6EZ44HY.js} +3 -3
  45. package/dist/{delete-dataset-records-WPMU5GL5.js → delete-dataset-records-7CNOTALB.js} +3 -3
  46. package/dist/{delete-evaluator-33SP2JFX.js → delete-evaluator-RKYC4TNB.js} +3 -3
  47. package/dist/{delete-prompt-tag-KNMZVSPB.js → delete-prompt-tag-QB3OCZUV.js} +2 -2
  48. package/dist/{delete-workflow-3NUXO46U.js → delete-workflow-NG2VXYYM.js} +3 -3
  49. package/dist/{discover-evaluator-schema-XTAD4X7D.js → discover-evaluator-schema-ESSHQGPY.js} +7 -286
  50. package/dist/{discover-scenario-schema-FE64QK2D.js → discover-scenario-schema-W7KBXA4J.js} +53 -1
  51. package/dist/get-agent-7THF7HCU.js +66 -0
  52. package/dist/{get-analytics-TETCSOK6.js → get-analytics-R6AZD3AW.js} +2 -2
  53. package/dist/{get-dashboard-KHBEGZGL.js → get-dashboard-P4WWVHFU.js} +3 -3
  54. package/dist/{get-dataset-RCMU5VAH.js → get-dataset-AKTYSQDI.js} +3 -3
  55. package/dist/{get-evaluator-ADMKVWYZ.js → get-evaluator-RZ44UIN6.js} +3 -3
  56. package/dist/{get-project-RAK7URQJ.js → get-project-QSQEAB33.js} +3 -3
  57. package/dist/get-prompt-GVKXF5NL.js +100 -0
  58. package/dist/get-run-plan-NY7J5GRE.js +61 -0
  59. package/dist/{get-scenario-ZTAJJL5T.js → get-scenario-Q3LLQE4T.js} +7 -3
  60. package/dist/{get-simulation-run-T2ZQWYH2.js → get-simulation-run-WINEYDMK.js} +12 -3
  61. package/dist/get-test-suite-4HKA7OKS.js +49 -0
  62. package/dist/{get-trace-K7U234Q4.js → get-trace-RCUV7N4O.js} +2 -2
  63. package/dist/{get-workflow-L5L2PSK3.js → get-workflow-EEOC7Y45.js} +3 -3
  64. package/dist/{http-server-YXHFPPGA.js → http-server-WCDUARTH.js} +8 -3
  65. package/dist/index.js +3366 -1910
  66. package/dist/{langwatch-api-annotations-FDAS6NQ7.js → langwatch-api-annotations-6DHVZWUK.js} +3 -3
  67. package/dist/{langwatch-api-dashboards-JTVCS7ZJ.js → langwatch-api-dashboards-2KJL2M42.js} +3 -3
  68. package/dist/{langwatch-api-monitors-3J7UIQME.js → langwatch-api-monitors-6ISFJ7WX.js} +2 -2
  69. package/dist/{langwatch-api-secrets-BUBSNCNO.js → langwatch-api-secrets-OEBD6GBU.js} +2 -2
  70. package/dist/{langwatch-api-triggers-EDFUCJN6.js → langwatch-api-triggers-5E6DDGX6.js} +2 -2
  71. package/dist/{langwatch-api-workflows-NBJX5KOG.js → langwatch-api-workflows-6UJL5U4Z.js} +3 -3
  72. package/dist/list-agents-AMVDQPQL.js +43 -0
  73. package/dist/{list-annotations-ZTVP4EEL.js → list-annotations-6PR5NLIC.js} +3 -3
  74. package/dist/{list-api-keys-CWD4UQT5.js → list-api-keys-GVPNBIHQ.js} +3 -3
  75. package/dist/{list-dashboards-PWG6P6KR.js → list-dashboards-Z3BZKDGF.js} +3 -3
  76. package/dist/{list-dataset-records-IOE7LQXS.js → list-dataset-records-O67HZ35Y.js} +3 -3
  77. package/dist/{list-datasets-ZT4JYUDT.js → list-datasets-WXZL6UMP.js} +3 -3
  78. package/dist/{list-evaluators-SJHC7VBH.js → list-evaluators-PAX72WCN.js} +3 -3
  79. package/dist/{list-model-providers-PPD2WPD2.js → list-model-providers-VVLZMLCX.js} +3 -3
  80. package/dist/{list-projects-K7QAIX2I.js → list-projects-AQVI6SUK.js} +3 -3
  81. package/dist/{list-prompt-tags-FOO6TNHB.js → list-prompt-tags-WZFBJOHG.js} +2 -2
  82. package/dist/{list-prompts-SC2HAUFG.js → list-prompts-BQOHRHYT.js} +2 -2
  83. package/dist/list-run-plans-UBAU2DGH.js +46 -0
  84. package/dist/{list-scenarios-F2WYNRIE.js → list-scenarios-MS4IUIVM.js} +8 -5
  85. package/dist/{list-simulation-runs-KF5THG5H.js → list-simulation-runs-D6YF4ZOU.js} +11 -5
  86. package/dist/list-test-suites-L7R2O6DR.js +35 -0
  87. package/dist/{list-workflows-QSJWURCK.js → list-workflows-6MV4XJHF.js} +3 -3
  88. package/dist/{rename-prompt-tag-EKWU5QF3.js → rename-prompt-tag-K5LQ4CY6.js} +2 -2
  89. package/dist/rename-test-suite-NTYHAO5X.js +21 -0
  90. package/dist/report-issue-RHZZ33IS.js +885 -0
  91. package/dist/rerun-run-plan-MUDWJBE4.js +22 -0
  92. package/dist/{revoke-api-key-FTRTZMOF.js → revoke-api-key-6LAZ3QZZ.js} +3 -3
  93. package/dist/run-agent-CAGWGTYT.js +72 -0
  94. package/dist/run-plan-T4ZYOZGA.js +39 -0
  95. package/dist/run-test-suite-L4MZ7G4D.js +26 -0
  96. package/dist/{run-workflow-QPXC627O.js → run-workflow-DFIS23AP.js} +3 -3
  97. package/dist/{search-traces-A6HFKPLJ.js → search-traces-4XTJYXIK.js} +2 -2
  98. package/dist/{set-model-provider-3WKT63ZN.js → set-model-provider-XRNMFOVR.js} +3 -3
  99. package/dist/{update-agent-SMAEHLXH.js → update-agent-DUWNYBNA.js} +3 -3
  100. package/dist/{update-dataset-BDUK7TUO.js → update-dataset-YSOMDCOB.js} +3 -3
  101. package/dist/{update-dataset-record-4ZSDRBSW.js → update-dataset-record-DEA2ITYR.js} +3 -3
  102. package/dist/{update-evaluator-LS6TIBY7.js → update-evaluator-NH7Q5GWV.js} +3 -3
  103. package/dist/{update-project-LKVYXENH.js → update-project-Q26FBSYH.js} +3 -3
  104. package/dist/update-prompt-ZQQ2C3KU.js +143 -0
  105. package/dist/{update-scenario-NEXCAQI7.js → update-scenario-WUX34JAR.js} +10 -3
  106. package/package.json +8 -9
  107. package/pyproject.toml +4 -4
  108. package/src/__tests__/agent-tools.unit.test.ts +257 -0
  109. package/src/__tests__/all-tools.integration.test.ts +321 -75
  110. package/src/__tests__/integration.integration.test.ts +7 -11
  111. package/src/__tests__/prompt-response-types.unit.test.ts +51 -0
  112. package/src/__tests__/prompts.integration.test.ts +736 -0
  113. package/src/__tests__/run-plan-tools.unit.test.ts +565 -0
  114. package/src/__tests__/scenario-tools.unit.test.ts +19 -0
  115. package/src/__tests__/simulation-run-tools.unit.test.ts +116 -0
  116. package/src/__tests__/test-suite-tools.unit.test.ts +615 -0
  117. package/src/__tests__/{tools.unit.test.ts → tools.integration.test.ts} +31 -59
  118. package/src/create-mcp-server.ts +334 -55
  119. package/src/langwatch-api-agents.ts +196 -12
  120. package/src/langwatch-api-run-plans.ts +157 -0
  121. package/src/langwatch-api-scenarios.ts +9 -0
  122. package/src/langwatch-api-simulation-runs.ts +5 -0
  123. package/src/langwatch-api-test-suites.ts +135 -0
  124. package/src/langwatch-api.ts +62 -12
  125. package/src/schemas/index.ts +5 -0
  126. package/src/schemas/run-plan.ts +92 -0
  127. package/src/schemas/suite-fields.ts +149 -0
  128. package/src/tools/archive-run-plan.ts +12 -0
  129. package/src/tools/archive-test-suite.ts +12 -0
  130. package/src/tools/create-scenario.ts +8 -0
  131. package/src/tools/create-test-suite.ts +43 -0
  132. package/src/tools/discover-scenario-schema.ts +54 -0
  133. package/src/tools/format-run-plan.ts +68 -0
  134. package/src/tools/format-scenario.ts +14 -0
  135. package/src/tools/format-suite-details.ts +96 -0
  136. package/src/tools/get-agent.ts +45 -3
  137. package/src/tools/get-prompt.ts +108 -24
  138. package/src/tools/get-run-plan.ts +62 -0
  139. package/src/tools/get-scenario.ts +3 -0
  140. package/src/tools/get-simulation-run.ts +9 -0
  141. package/src/tools/get-test-suite.ts +49 -0
  142. package/src/tools/list-agents.ts +20 -3
  143. package/src/tools/list-run-plans.ts +47 -0
  144. package/src/tools/list-scenarios.ts +12 -2
  145. package/src/tools/list-simulation-runs.ts +6 -2
  146. package/src/tools/list-test-suites.ts +35 -0
  147. package/src/tools/rename-test-suite.ts +18 -0
  148. package/src/tools/rerun-run-plan.ts +21 -0
  149. package/src/tools/run-agent.ts +54 -6
  150. package/src/tools/run-plan.ts +49 -0
  151. package/src/tools/run-test-suite.ts +30 -0
  152. package/src/tools/test-agent.ts +18 -0
  153. package/src/tools/update-prompt.ts +195 -10
  154. package/src/tools/update-scenario.ts +8 -0
  155. package/src/tools/update-test-suite.ts +45 -0
  156. package/tsconfig.json +5 -0
  157. package/uv.lock +416 -296
  158. package/dist/archive-suite-4T6BSRS7.js +0 -16
  159. package/dist/chunk-YZLYHLLI.js +0 -48
  160. package/dist/create-suite-LZS6QIAF.js +0 -39
  161. package/dist/get-agent-Y7MWZ65F.js +0 -29
  162. package/dist/get-prompt-OY5B7TRI.js +0 -46
  163. package/dist/get-suite-J7JVWO6O.js +0 -45
  164. package/dist/list-agents-KFHKGPXV.js +0 -33
  165. package/dist/list-suites-CRY4VWW6.js +0 -43
  166. package/dist/report-issue-XYVAPAYJ.js +0 -412
  167. package/dist/run-agent-6SM4XNA7.js +0 -37
  168. package/dist/run-suite-NKZCJ2IN.js +0 -30
  169. package/dist/update-prompt-ODCRQRN6.js +0 -25
  170. package/dist/update-suite-SNO3GS7T.js +0 -40
  171. package/src/langwatch-api-suites.ts +0 -118
  172. package/src/tools/archive-suite.ts +0 -12
  173. package/src/tools/create-suite.ts +0 -42
  174. package/src/tools/get-suite.ts +0 -47
  175. package/src/tools/list-suites.ts +0 -43
  176. package/src/tools/run-suite.ts +0 -28
  177. package/src/tools/update-suite.ts +0 -44
@@ -0,0 +1,257 @@
1
+ /**
2
+ * The agent tools with a connected agent: the fields the listing and the
3
+ * detail expose, and the relay a run goes through.
4
+ *
5
+ * @see specs/mcp-server/agent-tools.feature
6
+ */
7
+ import { beforeEach, describe, expect, it, vi } from "vitest";
8
+
9
+ vi.mock("../langwatch-api.js", async (importOriginal) => {
10
+ const actual = (await importOriginal()) as Record<string, unknown>;
11
+ return { ...actual, makeRequest: vi.fn() };
12
+ });
13
+ vi.mock("../public-http-request.js", () => ({ requestPublicJson: vi.fn() }));
14
+
15
+ import { makeRequest } from "../langwatch-api.js";
16
+ import { runAgent, type AgentSummary } from "../langwatch-api-agents.js";
17
+ import { requestPublicJson } from "../public-http-request.js";
18
+ import { runPlanTargetSchema, toWireTargets } from "../schemas/run-plan.js";
19
+ import { handleGetAgent } from "../tools/get-agent.js";
20
+ import { handleListAgents } from "../tools/list-agents.js";
21
+ import { handleRunAgent } from "../tools/run-agent.js";
22
+ import { handleTestAgent } from "../tools/test-agent.js";
23
+
24
+ const mockRequest = vi.mocked(makeRequest);
25
+ const mockPublic = vi.mocked(requestPublicJson);
26
+
27
+ const connectedAgent = (overrides: Partial<AgentSummary> = {}): AgentSummary => ({
28
+ id: "agent_conn",
29
+ name: "support-agent",
30
+ type: "connected",
31
+ config: null,
32
+ createdAt: "2026-01-01T00:00:00Z",
33
+ updatedAt: "2026-01-02T00:00:00Z",
34
+ environment: "production",
35
+ status: "online",
36
+ instances: [
37
+ { id: "inst_1", hostname: "pod-a", label: "blue", connectedAt: "2026-01-02T00:00:00Z" },
38
+ { id: "inst_2", hostname: "pod-b", label: null, connectedAt: "2026-01-02T00:01:00Z" },
39
+ ],
40
+ owner: { userId: "u1", name: "Ada" },
41
+ parameters: [
42
+ { name: "model", type: "string", options: ["gpt-5", "gpt-5-mini"], default: "gpt-5-mini", required: false },
43
+ { name: "plan", type: "string", required: true, description: "Customer plan" },
44
+ ],
45
+ ...overrides,
46
+ });
47
+
48
+ const httpAgent = (): AgentSummary => ({
49
+ id: "agent_http",
50
+ name: "Legacy",
51
+ type: "http",
52
+ config: { url: "https://api.example.com/agent" },
53
+ createdAt: "2026-01-01T00:00:00Z",
54
+ updatedAt: "2026-01-02T00:00:00Z",
55
+ });
56
+
57
+ beforeEach(() => {
58
+ vi.clearAllMocks();
59
+ });
60
+
61
+ describe("handleListAgents()", () => {
62
+ describe("when a connected agent and an HTTP agent are listed", () => {
63
+ /** @scenario "The listing shows environment, status, instances and owner" */
64
+ /** @scenario "The listing keeps the HTTP agent entry unchanged" */
65
+ it("shows the connected fields on the connected entry only", async () => {
66
+ mockRequest.mockResolvedValueOnce({
67
+ data: [connectedAgent(), httpAgent()],
68
+ pagination: { page: 1, limit: 100, total: 2, totalPages: 1 },
69
+ });
70
+
71
+ const output = await handleListAgents();
72
+
73
+ const [, connected, http] = output.split("## ");
74
+ expect(connected).toContain("**Environment**: production");
75
+ expect(connected).toContain("**Status**: online");
76
+ expect(connected).toContain("**Instances**: 2");
77
+ expect(connected).toContain("**Owner**: Ada");
78
+ expect(http).toContain("**Type**: http");
79
+ expect(http).not.toContain("Environment");
80
+ expect(http).not.toContain("Status");
81
+ });
82
+ });
83
+
84
+ describe("when a listed agent belongs to another person", () => {
85
+ /** @scenario "A listed agent the key cannot choose says so" */
86
+ it("lists it with its owner and says only its owner can run it", async () => {
87
+ mockRequest.mockResolvedValueOnce({
88
+ data: [connectedAgent({ selectable: false, notSelectableReason: "owned_by_another_person" })],
89
+ pagination: { page: 1, limit: 100, total: 1, totalPages: 1 },
90
+ });
91
+
92
+ const output = await handleListAgents();
93
+
94
+ expect(output).toContain("**Owner**: Ada");
95
+ expect(output).toContain("**Run**: only its owner can run this agent");
96
+ });
97
+ });
98
+ });
99
+
100
+ describe("handleGetAgent()", () => {
101
+ describe("when the agent declares parameters and has instances", () => {
102
+ /** @scenario "The detail shows the parameters and the instances" */
103
+ it("lists each parameter with its type, options, default and required, and each instance", async () => {
104
+ mockRequest.mockResolvedValueOnce(connectedAgent());
105
+
106
+ const output = await handleGetAgent({ id: "agent_conn" });
107
+
108
+ expect(output).toContain("## Parameters");
109
+ expect(output).toContain('- **model** (string, one of gpt-5, gpt-5-mini, default "gpt-5-mini")');
110
+ expect(output).toContain("- **plan** (string, required): Customer plan");
111
+ expect(output).toContain("## Instances (2)");
112
+ expect(output).toContain("- pod-a (blue), connected 2026-01-02T00:00:00Z");
113
+ expect(output).toContain("- pod-b, connected 2026-01-02T00:01:00Z");
114
+ expect(output).toContain("**Owner**: Ada");
115
+ });
116
+ });
117
+
118
+ });
119
+
120
+ describe("handleTestAgent()", () => {
121
+ describe("when an agent is tested", () => {
122
+ /** @scenario "The REST route schedules the same run" */
123
+ it("posts to the test route and reports the run ids to follow", async () => {
124
+ mockRequest.mockResolvedValueOnce({
125
+ scenarioRunId: "run_1",
126
+ batchRunId: "batch_1",
127
+ setId: "__internal__proj_1__agent-test",
128
+ });
129
+
130
+ const output = await handleTestAgent({ id: "agent_http" });
131
+
132
+ expect(mockRequest).toHaveBeenCalledWith("POST", "/api/v1/agents/agent_http/test", {});
133
+ expect(output).toContain("**Scenario run ID:** run_1");
134
+ expect(output).toContain("**Batch run ID:** batch_1");
135
+ expect(output).toContain("platform_get_simulation_run");
136
+ });
137
+ });
138
+ });
139
+
140
+ describe("handleRunAgent()", () => {
141
+ describe("when the agent is connected and online", () => {
142
+ /** @scenario "A message runs one turn on a live instance" */
143
+ it("calls the relay with one user message and the parameters, and reports the instance", async () => {
144
+ mockRequest.mockResolvedValueOnce(connectedAgent());
145
+ mockRequest.mockResolvedValueOnce({
146
+ output: "Sure, I can help.",
147
+ session: { id: "s1" },
148
+ instance: { hostname: "pod-a", label: "blue" },
149
+ durationMs: 321,
150
+ });
151
+
152
+ const output = await handleRunAgent({ id: "agent_conn", message: "hi", parameters: { model: "gpt-5" } });
153
+
154
+ expect(mockRequest).toHaveBeenNthCalledWith(2, "POST", "/api/v1/agents/agent_conn/call", {
155
+ messages: [{ role: "user", content: "hi" }],
156
+ params: { model: "gpt-5" },
157
+ });
158
+ expect(output).toContain("type: connected");
159
+ expect(output).toContain("Sure, I can help.");
160
+ expect(output).toContain("**Instance:** pod-a (blue)");
161
+ expect(output).toContain("**Duration:** 321 ms");
162
+ });
163
+
164
+ /** @scenario "An input with messages is sent as the relay body" */
165
+ it("sends an input with messages, thread id and session as the body", async () => {
166
+ mockRequest.mockResolvedValueOnce(connectedAgent());
167
+ mockRequest.mockResolvedValueOnce({ output: "ok", instance: { hostname: "pod-a" }, durationMs: 1 });
168
+ const input = JSON.stringify({
169
+ messages: [{ role: "user", content: "again" }],
170
+ threadId: "t1",
171
+ session: { id: "s1" },
172
+ });
173
+
174
+ await handleRunAgent({ id: "agent_conn", input, threadId: "t2" });
175
+
176
+ expect(mockRequest).toHaveBeenNthCalledWith(2, "POST", "/api/v1/agents/agent_conn/call", {
177
+ messages: [{ role: "user", content: "again" }],
178
+ threadId: "t2",
179
+ session: { id: "s1" },
180
+ });
181
+ });
182
+
183
+ /** @scenario "A connected agent needs a conversation" */
184
+ it("refuses an input with no messages and no message", async () => {
185
+ mockRequest.mockResolvedValueOnce(connectedAgent());
186
+
187
+ await expect(runAgent({ id: "agent_conn", input: { question: "hi" } })).rejects.toThrow(/give `message`, or `input` with a `messages` list/);
188
+ expect(mockRequest).toHaveBeenCalledTimes(1);
189
+ });
190
+
191
+ it("refuses an offline agent before the relay is called", async () => {
192
+ mockRequest.mockResolvedValueOnce(connectedAgent({ status: "offline", instances: [] }));
193
+
194
+ await expect(runAgent({ id: "agent_conn", message: "hi" })).rejects.toThrow(/is offline/);
195
+ expect(mockRequest).toHaveBeenCalledTimes(1);
196
+ });
197
+
198
+ /** @scenario "A nested params object is refused before the relay" */
199
+ it("refuses an input whose params carry a nested object", async () => {
200
+ mockRequest.mockResolvedValueOnce(connectedAgent());
201
+
202
+ await expect(
203
+ runAgent({
204
+ id: "agent_conn",
205
+ input: { messages: [{ role: "user", content: "hi" }], params: { model: { name: "gpt-5" } } },
206
+ }),
207
+ ).rejects.toThrow(/flat object of string, number or boolean values/);
208
+ expect(mockRequest).toHaveBeenCalledTimes(1);
209
+ });
210
+ });
211
+
212
+ describe("when the input is JSON that is not an object", () => {
213
+ /** @scenario "An input that is not a JSON object is refused" */
214
+ it("refuses a scalar, an array and null before the agent is read", async () => {
215
+ for (const input of ["5", '"hi"', "[1,2]", "null"]) {
216
+ expect(await handleRunAgent({ id: "agent_conn", input })).toBe(
217
+ "Error: `input` must be a valid JSON object.",
218
+ );
219
+ }
220
+ expect(mockRequest).not.toHaveBeenCalled();
221
+ });
222
+ });
223
+
224
+ describe("when the agent is an HTTP agent", () => {
225
+ /** @scenario "An HTTP agent is still called at its URL" */
226
+ it("calls the URL directly and not the relay", async () => {
227
+ mockRequest.mockResolvedValueOnce(httpAgent());
228
+ mockPublic.mockResolvedValueOnce({ answer: "hi" });
229
+
230
+ const output = await handleRunAgent({ id: "agent_http", input: '{"question":"hi"}' });
231
+
232
+ expect(mockPublic).toHaveBeenCalledWith("https://api.example.com/agent", expect.objectContaining({ method: "POST" }));
233
+ expect(mockRequest).toHaveBeenCalledTimes(1);
234
+ expect(output).toContain("type: http");
235
+ });
236
+ });
237
+ });
238
+
239
+ describe("runPlanTargetSchema", () => {
240
+ /** @scenario "A connected target names an agent by id" */
241
+ it("accepts a connected target with an agent id", () => {
242
+ expect(runPlanTargetSchema.parse({ type: "connected", referenceId: "agent_conn" })).toEqual({
243
+ type: "connected",
244
+ referenceId: "agent_conn",
245
+ });
246
+ });
247
+
248
+ /** @scenario "A connected target names an agent by name and environment" */
249
+ it("passes name@environment through as the reference id", () => {
250
+ const targets = toWireTargets([
251
+ runPlanTargetSchema.parse({ type: "connected", referenceId: "support-agent@production", parameters: { model: "gpt-5" } }),
252
+ ]);
253
+ expect(targets).toEqual([
254
+ { type: "connected", referenceId: "support-agent@production", runParameters: { model: "gpt-5" } },
255
+ ]);
256
+ });
257
+ });