@langwatch/mcp-server 1.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 (204) hide show
  1. package/.python-version +1 -0
  2. package/CHANGELOG.md +110 -0
  3. package/CONTRIBUTING.md +1 -1
  4. package/README.md +86 -4
  5. package/dist/{archive-project-MK3673A7.js → archive-project-W3IKI7UL.js} +5 -5
  6. package/dist/archive-run-plan-474OKHHU.js +16 -0
  7. package/dist/{archive-scenario-KATSXRIN.js → archive-scenario-V5UNYK26.js} +5 -5
  8. package/dist/archive-test-suite-O7FLDMXK.js +16 -0
  9. package/dist/{assign-prompt-tag-E3C5W6PI.js → assign-prompt-tag-Q46WRB6P.js} +4 -4
  10. package/dist/chunk-56OGTS24.js +44 -0
  11. package/dist/chunk-6DJDEXET.js +63 -0
  12. package/dist/chunk-7E4FZIYJ.js +15 -0
  13. package/dist/{chunk-J45SVWPR.js → chunk-AN4OUA4D.js} +2 -2
  14. package/dist/chunk-CKNAH5TG.js +74 -0
  15. package/dist/{chunk-CE6QMZOP.js → chunk-EYXMLE5X.js} +86 -11
  16. package/dist/{chunk-XY4STOBE.js → chunk-G4ZAZTZW.js} +2 -2
  17. package/dist/{chunk-5UWY24RP.js → chunk-GELJ2MQR.js} +12 -3
  18. package/dist/chunk-HV4DWKBI.js +46 -0
  19. package/dist/chunk-JR6DJZOU.js +49 -0
  20. package/dist/{chunk-DAIQKS27.js → chunk-JTQJRNFJ.js} +2 -2
  21. package/dist/{chunk-WWMDHZZV.js → chunk-K37ZNPXK.js} +2 -2
  22. package/dist/{chunk-EU22L52K.js → chunk-OJLYYJWI.js} +2 -2
  23. package/dist/{chunk-Y3HDZ6OW.js → chunk-QAMP3CGR.js} +2 -2
  24. package/dist/{chunk-VYPSKRPN.js → chunk-QRO35SOM.js} +347 -146
  25. package/dist/{chunk-O3LWVN67.js → chunk-RVAE2MEA.js} +2 -2
  26. package/dist/{chunk-UXXA4D6K.js → chunk-S4GVX36W.js} +2 -2
  27. package/dist/{chunk-BJ7B4R26.js → chunk-SAS5OB7F.js} +18 -2
  28. package/dist/{chunk-RYRB2YVJ.js → chunk-SR6QPRIE.js} +2 -2
  29. package/dist/chunk-UF6XIGYR.js +58 -0
  30. package/dist/{chunk-T7WJVS2N.js → chunk-UFOYFYI7.js} +2 -2
  31. package/dist/chunk-UJPXFA26.js +267 -0
  32. package/dist/{chunk-KOO5733S.js → chunk-WT4PO4A3.js} +8 -2
  33. package/dist/config.js +2 -2
  34. package/dist/{create-agent-7W57J7MT.js → create-agent-JFJ2K6X3.js} +5 -5
  35. package/dist/{create-annotation-W2XELBCQ.js → create-annotation-EUXXQ6NS.js} +5 -5
  36. package/dist/{create-api-key-MKRXKFJM.js → create-api-key-BGBB76JX.js} +5 -5
  37. package/dist/{create-dashboard-QGL7IODC.js → create-dashboard-Z76AIPLS.js} +5 -5
  38. package/dist/{create-dataset-3PH7A4HG.js → create-dataset-FD5VAXU4.js} +5 -5
  39. package/dist/{create-dataset-records-4DZS3VRL.js → create-dataset-records-WQUXXOHI.js} +5 -5
  40. package/dist/{create-evaluator-YWH6NC4M.js → create-evaluator-6NV33Z52.js} +5 -5
  41. package/dist/create-mcp-server.js +9 -4
  42. package/dist/{create-project-BOBQ32C5.js → create-project-ZQ7TFIO2.js} +5 -5
  43. package/dist/{create-prompt-FLCFGITE.js → create-prompt-BS6QRCUU.js} +4 -4
  44. package/dist/{create-prompt-tag-EMPNIKEA.js → create-prompt-tag-FCQ5ELSQ.js} +4 -4
  45. package/dist/{create-scenario-BSBIHUL6.js → create-scenario-DRPTPXU3.js} +12 -5
  46. package/dist/create-test-suite-ZIZCCFJ4.js +37 -0
  47. package/dist/{delete-agent-7MSYNY5E.js → delete-agent-SA4JMUZF.js} +5 -5
  48. package/dist/{delete-annotation-ZT5IGN24.js → delete-annotation-X3KKK7QY.js} +5 -5
  49. package/dist/{delete-dashboard-BTIY5S3L.js → delete-dashboard-LYMRPF27.js} +5 -5
  50. package/dist/{delete-dataset-U45HQHPR.js → delete-dataset-W6EZ44HY.js} +5 -5
  51. package/dist/{delete-dataset-records-R43IMAWB.js → delete-dataset-records-7CNOTALB.js} +5 -5
  52. package/dist/{delete-evaluator-U6JMQ6C7.js → delete-evaluator-RKYC4TNB.js} +5 -5
  53. package/dist/{delete-prompt-tag-4EV4TDHB.js → delete-prompt-tag-QB3OCZUV.js} +4 -4
  54. package/dist/{delete-workflow-5QYALXEL.js → delete-workflow-NG2VXYYM.js} +5 -5
  55. package/dist/{discover-evaluator-schema-MRGHVNTT.js → discover-evaluator-schema-ESSHQGPY.js} +58 -321
  56. package/dist/{discover-scenario-schema-UPBTCWPG.js → discover-scenario-schema-W7KBXA4J.js} +54 -2
  57. package/dist/{discover-schema-ICGXPHUW.js → discover-schema-L6QDPON3.js} +2 -2
  58. package/dist/get-agent-7THF7HCU.js +66 -0
  59. package/dist/{get-analytics-PVERWOA5.js → get-analytics-R6AZD3AW.js} +4 -4
  60. package/dist/{get-dashboard-ZZPC3WMD.js → get-dashboard-P4WWVHFU.js} +5 -5
  61. package/dist/{get-dataset-25TB2LQH.js → get-dataset-AKTYSQDI.js} +5 -5
  62. package/dist/{get-evaluator-3FTRQKEC.js → get-evaluator-RZ44UIN6.js} +5 -5
  63. package/dist/{get-project-5OAXYC57.js → get-project-QSQEAB33.js} +5 -5
  64. package/dist/get-prompt-GVKXF5NL.js +100 -0
  65. package/dist/get-run-plan-NY7J5GRE.js +61 -0
  66. package/dist/{get-scenario-DSSTR6FI.js → get-scenario-Q3LLQE4T.js} +9 -5
  67. package/dist/{get-simulation-run-RQPMS2MC.js → get-simulation-run-WINEYDMK.js} +14 -5
  68. package/dist/get-test-suite-4HKA7OKS.js +49 -0
  69. package/dist/{get-trace-O5VT3NAC.js → get-trace-RCUV7N4O.js} +4 -4
  70. package/dist/{get-workflow-3EZDOEIF.js → get-workflow-EEOC7Y45.js} +5 -5
  71. package/dist/http-security-EU3CHTY7.js +26 -0
  72. package/dist/{http-server-2ACJ3CXW.js → http-server-WCDUARTH.js} +1038 -693
  73. package/dist/index.js +6437 -4020
  74. package/dist/{langwatch-api-annotations-E65MI3WZ.js → langwatch-api-annotations-6DHVZWUK.js} +5 -5
  75. package/dist/{langwatch-api-dashboards-QCDECRCY.js → langwatch-api-dashboards-2KJL2M42.js} +5 -5
  76. package/dist/{langwatch-api-monitors-UIO2KTDV.js → langwatch-api-monitors-6ISFJ7WX.js} +4 -4
  77. package/dist/{langwatch-api-secrets-4T2EKSJ7.js → langwatch-api-secrets-OEBD6GBU.js} +4 -4
  78. package/dist/{langwatch-api-triggers-MPOEKPUY.js → langwatch-api-triggers-5E6DDGX6.js} +4 -4
  79. package/dist/langwatch-api-workflows-6UJL5U4Z.js +16 -0
  80. package/dist/list-agents-AMVDQPQL.js +43 -0
  81. package/dist/{list-annotations-CGX5BT7E.js → list-annotations-6PR5NLIC.js} +5 -5
  82. package/dist/{list-api-keys-JLWJPFRX.js → list-api-keys-GVPNBIHQ.js} +5 -5
  83. package/dist/{list-dashboards-RNIWX3TX.js → list-dashboards-Z3BZKDGF.js} +5 -5
  84. package/dist/{list-dataset-records-B5MHZR66.js → list-dataset-records-O67HZ35Y.js} +5 -5
  85. package/dist/{list-datasets-BVCAMQCP.js → list-datasets-WXZL6UMP.js} +5 -5
  86. package/dist/{list-evaluators-65FBTL2U.js → list-evaluators-PAX72WCN.js} +5 -5
  87. package/dist/{list-model-providers-GRK3I3VV.js → list-model-providers-VVLZMLCX.js} +5 -5
  88. package/dist/{list-projects-JLMBFMW3.js → list-projects-AQVI6SUK.js} +5 -5
  89. package/dist/{list-prompt-tags-UHNWDGQI.js → list-prompt-tags-WZFBJOHG.js} +4 -4
  90. package/dist/{list-prompts-7254B3U7.js → list-prompts-BQOHRHYT.js} +4 -4
  91. package/dist/list-run-plans-UBAU2DGH.js +46 -0
  92. package/dist/{list-scenarios-46YNICKM.js → list-scenarios-MS4IUIVM.js} +10 -7
  93. package/dist/{list-simulation-runs-LGNEFNPN.js → list-simulation-runs-D6YF4ZOU.js} +13 -7
  94. package/dist/list-test-suites-L7R2O6DR.js +35 -0
  95. package/dist/{list-workflows-MLQDM6CP.js → list-workflows-6MV4XJHF.js} +5 -5
  96. package/dist/{rename-prompt-tag-RHQMNEQL.js → rename-prompt-tag-K5LQ4CY6.js} +4 -4
  97. package/dist/rename-test-suite-NTYHAO5X.js +21 -0
  98. package/dist/report-issue-RHZZ33IS.js +885 -0
  99. package/dist/rerun-run-plan-MUDWJBE4.js +22 -0
  100. package/dist/{revoke-api-key-3Y35EMBT.js → revoke-api-key-6LAZ3QZZ.js} +5 -5
  101. package/dist/run-agent-CAGWGTYT.js +72 -0
  102. package/dist/run-plan-T4ZYOZGA.js +39 -0
  103. package/dist/run-test-suite-L4MZ7G4D.js +26 -0
  104. package/dist/{run-workflow-5LVBZYLS.js → run-workflow-DFIS23AP.js} +5 -5
  105. package/dist/{search-traces-U4HLOCZ2.js → search-traces-4XTJYXIK.js} +4 -4
  106. package/dist/{set-model-provider-V3M5CDIY.js → set-model-provider-XRNMFOVR.js} +5 -5
  107. package/dist/{update-agent-47RZIHS7.js → update-agent-DUWNYBNA.js} +5 -5
  108. package/dist/{update-dataset-G6QJJGS5.js → update-dataset-YSOMDCOB.js} +5 -5
  109. package/dist/{update-dataset-record-RMWLSWCS.js → update-dataset-record-DEA2ITYR.js} +5 -5
  110. package/dist/{update-evaluator-SFRRI5Q3.js → update-evaluator-NH7Q5GWV.js} +5 -5
  111. package/dist/{update-project-PI5DWBVK.js → update-project-Q26FBSYH.js} +5 -5
  112. package/dist/update-prompt-ZQQ2C3KU.js +143 -0
  113. package/dist/{update-scenario-G3IU2UIB.js → update-scenario-WUX34JAR.js} +12 -5
  114. package/package.json +8 -9
  115. package/pyproject.toml +5 -5
  116. package/src/__tests__/agent-tools.unit.test.ts +257 -0
  117. package/src/__tests__/all-tools.integration.test.ts +321 -75
  118. package/src/__tests__/config.unit.test.ts +33 -0
  119. package/src/__tests__/docker.integration.test.ts +91 -7
  120. package/src/__tests__/documentation-fetch-security.integration.test.ts +8 -2
  121. package/src/__tests__/feature-map-tool-drift.unit.test.ts +93 -0
  122. package/src/__tests__/http-security-origin.unit.test.ts +97 -0
  123. package/src/__tests__/http-security-sessions.unit.test.ts +209 -0
  124. package/src/__tests__/http-security-verifier.unit.test.ts +248 -0
  125. package/src/__tests__/http-server-allocation.integration.test.ts +279 -0
  126. package/src/__tests__/http-server-origin.integration.test.ts +169 -0
  127. package/src/__tests__/http-server-session-auth.integration.test.ts +341 -0
  128. package/src/__tests__/http-transport.integration.test.ts +85 -87
  129. package/src/__tests__/integration.integration.test.ts +7 -11
  130. package/src/__tests__/prompt-response-types.unit.test.ts +51 -0
  131. package/src/__tests__/prompts.integration.test.ts +736 -0
  132. package/src/__tests__/run-plan-tools.unit.test.ts +565 -0
  133. package/src/__tests__/scenario-tools.unit.test.ts +19 -0
  134. package/src/__tests__/simulation-run-tools.unit.test.ts +116 -0
  135. package/src/__tests__/support/http-server-harness.ts +138 -0
  136. package/src/__tests__/test-suite-tools.unit.test.ts +615 -0
  137. package/src/__tests__/{tools.unit.test.ts → tools.integration.test.ts} +31 -59
  138. package/src/config.ts +17 -2
  139. package/src/create-mcp-server.ts +334 -55
  140. package/src/http-security.ts +492 -0
  141. package/src/http-server.ts +536 -152
  142. package/src/index.ts +49 -2
  143. package/src/langwatch-api-agents.ts +196 -12
  144. package/src/langwatch-api-run-plans.ts +157 -0
  145. package/src/langwatch-api-scenarios.ts +9 -0
  146. package/src/langwatch-api-simulation-runs.ts +5 -0
  147. package/src/langwatch-api-test-suites.ts +135 -0
  148. package/src/langwatch-api.ts +62 -12
  149. package/src/schemas/index.ts +5 -0
  150. package/src/schemas/run-plan.ts +92 -0
  151. package/src/schemas/suite-fields.ts +149 -0
  152. package/src/tools/archive-run-plan.ts +12 -0
  153. package/src/tools/archive-test-suite.ts +12 -0
  154. package/src/tools/create-scenario.ts +8 -0
  155. package/src/tools/create-test-suite.ts +43 -0
  156. package/src/tools/discover-evaluator-schema.ts +2 -2
  157. package/src/tools/discover-scenario-schema.ts +54 -0
  158. package/src/tools/format-run-plan.ts +68 -0
  159. package/src/tools/format-scenario.ts +14 -0
  160. package/src/tools/format-suite-details.ts +96 -0
  161. package/src/tools/get-agent.ts +45 -3
  162. package/src/tools/get-prompt.ts +108 -24
  163. package/src/tools/get-run-plan.ts +62 -0
  164. package/src/tools/get-scenario.ts +3 -0
  165. package/src/tools/get-simulation-run.ts +9 -0
  166. package/src/tools/get-test-suite.ts +49 -0
  167. package/src/tools/list-agents.ts +20 -3
  168. package/src/tools/list-run-plans.ts +47 -0
  169. package/src/tools/list-scenarios.ts +12 -2
  170. package/src/tools/list-simulation-runs.ts +6 -2
  171. package/src/tools/list-test-suites.ts +35 -0
  172. package/src/tools/rename-test-suite.ts +18 -0
  173. package/src/tools/rerun-run-plan.ts +21 -0
  174. package/src/tools/run-agent.ts +54 -6
  175. package/src/tools/run-plan.ts +49 -0
  176. package/src/tools/run-test-suite.ts +30 -0
  177. package/src/tools/test-agent.ts +18 -0
  178. package/src/tools/update-prompt.ts +195 -10
  179. package/src/tools/update-scenario.ts +8 -0
  180. package/src/tools/update-test-suite.ts +45 -0
  181. package/tsconfig.json +7 -2
  182. package/uv.lock +418 -298
  183. package/dist/archive-suite-C3KX6IIZ.js +0 -16
  184. package/dist/chunk-WDH5P5RF.js +0 -48
  185. package/dist/create-suite-TWF6BBR4.js +0 -39
  186. package/dist/get-agent-7QJVPH72.js +0 -29
  187. package/dist/get-prompt-I753LZKZ.js +0 -46
  188. package/dist/get-suite-GTOCY5PL.js +0 -45
  189. package/dist/langwatch-api-workflows-5HRZ7HR4.js +0 -16
  190. package/dist/list-agents-TIJTP566.js +0 -33
  191. package/dist/list-suites-5VK4HXEK.js +0 -43
  192. package/dist/report-issue-DXJ6TPTC.js +0 -412
  193. package/dist/run-agent-RBSIFKRT.js +0 -37
  194. package/dist/run-suite-K3DNQ3SE.js +0 -30
  195. package/dist/update-prompt-RQFTEO7Y.js +0 -25
  196. package/dist/update-suite-TRGGLNR7.js +0 -40
  197. package/pnpm-workspace.yaml +0 -34
  198. package/src/langwatch-api-suites.ts +0 -118
  199. package/src/tools/archive-suite.ts +0 -12
  200. package/src/tools/create-suite.ts +0 -42
  201. package/src/tools/get-suite.ts +0 -47
  202. package/src/tools/list-suites.ts +0 -43
  203. package/src/tools/run-suite.ts +0 -28
  204. package/src/tools/update-suite.ts +0 -44
package/src/index.ts CHANGED
@@ -24,6 +24,17 @@ const argv = await yargs(hideBin(process.argv))
24
24
  description: "HTTP server port (only used with --http)",
25
25
  default: 3000,
26
26
  })
27
+ .option("host", {
28
+ type: "string",
29
+ description:
30
+ "HTTP listen address (only used with --http). Defaults to loopback; pass 0.0.0.0 to accept connections from other machines",
31
+ })
32
+ .option("allowedOrigin", {
33
+ type: "array",
34
+ string: true,
35
+ description:
36
+ "Browser origin allowed to call the HTTP server, repeatable. Loopback origins are always allowed",
37
+ })
27
38
  .help()
28
39
  .parseAsync();
29
40
 
@@ -34,11 +45,47 @@ initConfig({
34
45
 
35
46
  if (argv.http) {
36
47
  const { startHttpServer } = await import("./http-server.js");
37
- const { port } = await startHttpServer({ port: argv.port });
38
- console.log(`LangWatch MCP server listening on http://0.0.0.0:${port}/mcp`);
48
+ const { isLoopbackHost, parseAllowedOrigins } = await import(
49
+ "./http-security.js"
50
+ );
51
+
52
+ const allowedOrigins =
53
+ argv.allowedOrigin && argv.allowedOrigin.length > 0
54
+ ? parseAllowedOrigins(argv.allowedOrigin.join(","))
55
+ : undefined;
56
+
57
+ const { port, host } = await startHttpServer({
58
+ port: argv.port,
59
+ host: argv.host,
60
+ allowedOrigins,
61
+ });
62
+
63
+ // An IPv6 literal has to be bracketed inside a URL, and must not be
64
+ // double-bracketed when the operator already wrote it that way.
65
+ const displayHost =
66
+ host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
67
+ console.log(
68
+ `LangWatch MCP server listening on http://${displayHost}:${port}/mcp`
69
+ );
39
70
  console.log(
40
71
  "Clients must provide their API key via Authorization: Bearer <key> header"
41
72
  );
73
+ if (!isLoopbackHost(host)) {
74
+ console.warn(
75
+ `Warning: bound to ${host}, so the server is reachable from other machines. ` +
76
+ "Make sure it sits behind a trusted network boundary and that --allowedOrigin " +
77
+ "lists only origins you control."
78
+ );
79
+ }
80
+ if (process.env.LANGWATCH_MCP_TRUST_PROXY === "true") {
81
+ console.warn(
82
+ "Warning: LANGWATCH_MCP_TRUST_PROXY=true, so the rate limit on failed " +
83
+ "authentication uses the client address from X-Forwarded-For. Keep this " +
84
+ "on only when a trusted proxy terminates in front of the port and " +
85
+ "overwrites that header. Otherwise a client can supply any address there " +
86
+ "and the limit stops counting."
87
+ );
88
+ }
42
89
  } else {
43
90
  const transport = new StdioServerTransport();
44
91
  const server = createMcpServer();
@@ -1,6 +1,42 @@
1
+ import { z } from "zod";
2
+
1
3
  import { makeRequest } from "./langwatch-api.js";
2
4
  import { requestPublicJson } from "./public-http-request.js";
3
5
 
6
+ /**
7
+ * The run parameters a call carries: a flat record of scalars. The platform
8
+ * declares run parameters as `string`, `number` or `boolean`, so a nested
9
+ * object or an array is outside the contract and is refused here.
10
+ */
11
+ export const agentCallParamsSchema = z.record(
12
+ z.string(),
13
+ z.union([z.string(), z.number(), z.boolean()]),
14
+ );
15
+
16
+ export type AgentCallParams = z.infer<typeof agentCallParamsSchema>;
17
+
18
+ /**
19
+ * One run parameter an agent declares, as the platform lists it. An agent
20
+ * never declares a secret parameter: a secret belongs to the scenario and is
21
+ * supplied per run.
22
+ */
23
+ export interface AgentParameterSpec {
24
+ name: string;
25
+ type: "string" | "number" | "boolean";
26
+ options?: string[];
27
+ default?: string | number | boolean;
28
+ description?: string;
29
+ required?: boolean;
30
+ }
31
+
32
+ /** One process connected as an instance of a connected agent. */
33
+ export interface AgentInstance {
34
+ id: string;
35
+ hostname: string;
36
+ label?: string | null;
37
+ connectedAt: string;
38
+ }
39
+
4
40
  export interface AgentSummary {
5
41
  id: string;
6
42
  name: string;
@@ -8,6 +44,19 @@ export interface AgentSummary {
8
44
  config: Record<string, unknown> | null;
9
45
  createdAt: string;
10
46
  updatedAt: string;
47
+ /** Connected agents: the environment the SDK registered under. */
48
+ environment?: string | null;
49
+ /** Connected agents: online while at least one instance is connected. */
50
+ status?: "online" | "offline" | null;
51
+ instances?: AgentInstance[];
52
+ lastSeenAt?: string | null;
53
+ owner?: { userId: string; name: string } | null;
54
+ hostLabel?: string | null;
55
+ /** Whether this key can run the agent. */
56
+ selectable?: boolean;
57
+ /** Why the agent cannot be run with this key; null when it can. */
58
+ notSelectableReason?: "owned_by_another_person" | null;
59
+ parameters?: AgentParameterSpec[];
11
60
  }
12
61
 
13
62
  export interface AgentListResponse {
@@ -20,6 +69,31 @@ export interface AgentListResponse {
20
69
  };
21
70
  }
22
71
 
72
+ /** One conversation message as the relay carries it, OpenAI style. */
73
+ export interface AgentCallMessage {
74
+ role: string;
75
+ content?: unknown;
76
+ [key: string]: unknown;
77
+ }
78
+
79
+ /** The body `POST /api/v1/agents/:id/call` takes. */
80
+ export interface AgentCallBody {
81
+ messages: AgentCallMessage[];
82
+ newMessages?: AgentCallMessage[];
83
+ threadId?: string;
84
+ params?: AgentCallParams;
85
+ session?: unknown;
86
+ traceparent?: string;
87
+ }
88
+
89
+ /** The reply of the relay: the function's output and the instance that ran it. */
90
+ export interface AgentCallResponse {
91
+ output: unknown;
92
+ session?: unknown;
93
+ instance: { hostname: string; label?: string | null };
94
+ durationMs: number;
95
+ }
96
+
23
97
  export async function listAgents(params?: {
24
98
  page?: number;
25
99
  limit?: number;
@@ -28,13 +102,13 @@ export async function listAgents(params?: {
28
102
  if (params?.page) query.set("page", String(params.page));
29
103
  if (params?.limit) query.set("limit", String(params.limit));
30
104
  const qs = query.toString() ? `?${query}` : "";
31
- return makeRequest("GET", `/api/agents${qs}`) as Promise<AgentListResponse>;
105
+ return makeRequest("GET", `/api/v1/agents${qs}`) as Promise<AgentListResponse>;
32
106
  }
33
107
 
34
108
  export async function getAgent(id: string): Promise<AgentSummary> {
35
109
  return makeRequest(
36
110
  "GET",
37
- `/api/agents/${encodeURIComponent(id)}`,
111
+ `/api/v1/agents/${encodeURIComponent(id)}`,
38
112
  ) as Promise<AgentSummary>;
39
113
  }
40
114
 
@@ -43,7 +117,7 @@ export async function createAgent(data: {
43
117
  type: string;
44
118
  config: Record<string, unknown>;
45
119
  }): Promise<AgentSummary> {
46
- return makeRequest("POST", "/api/agents", data) as Promise<AgentSummary>;
120
+ return makeRequest("POST", "/api/v1/agents", data) as Promise<AgentSummary>;
47
121
  }
48
122
 
49
123
  export async function updateAgent(params: {
@@ -55,22 +129,132 @@ export async function updateAgent(params: {
55
129
  const { id, ...data } = params;
56
130
  return makeRequest(
57
131
  "PATCH",
58
- `/api/agents/${encodeURIComponent(id)}`,
132
+ `/api/v1/agents/${encodeURIComponent(id)}`,
59
133
  data,
60
134
  ) as Promise<AgentSummary>;
61
135
  }
62
136
 
63
137
  /**
64
- * Run an agent. For HTTP agents, calls the configured URL directly.
65
- * For workflow-linked agents, calls the workflow run endpoint.
138
+ * Runs one turn of a connected agent through the relay: the platform
139
+ * dispatches it to a live instance and answers with the function's output.
140
+ */
141
+ export async function callAgent({
142
+ id,
143
+ body,
144
+ }: {
145
+ id: string;
146
+ body: AgentCallBody;
147
+ }): Promise<AgentCallResponse> {
148
+ return makeRequest(
149
+ "POST",
150
+ `/api/v1/agents/${encodeURIComponent(id)}/call`,
151
+ body,
152
+ ) as Promise<AgentCallResponse>;
153
+ }
154
+
155
+ /** The ids `POST /api/v1/agents/:id/test` answers with. */
156
+ export interface AgentTestRunResponse {
157
+ scenarioRunId: string;
158
+ batchRunId: string;
159
+ setId: string;
160
+ }
161
+
162
+ /**
163
+ * Runs one scripted scenario against an agent: the user sends "ping", the
164
+ * agent answers, and the run succeeds when the answer arrives. The project
165
+ * gains no scenario, run plan or test suite, and the answer carries the run
166
+ * ids to follow.
167
+ */
168
+ export async function testAgent(id: string): Promise<AgentTestRunResponse> {
169
+ return makeRequest(
170
+ "POST",
171
+ `/api/v1/agents/${encodeURIComponent(id)}/test`,
172
+ {},
173
+ ) as Promise<AgentTestRunResponse>;
174
+ }
175
+
176
+ const isMessageList = (value: unknown): value is AgentCallMessage[] =>
177
+ Array.isArray(value) &&
178
+ value.every(
179
+ (item) =>
180
+ typeof item === "object" && item !== null && typeof (item as AgentCallMessage).role === "string",
181
+ );
182
+
183
+ /**
184
+ * The relay body for a connected agent: `message` is one user turn, `input`
185
+ * is the body itself (it must carry `messages`), `parameters` are the run
186
+ * parameters and `threadId` continues a conversation. A string is the
187
+ * refusal, so the tool can answer it.
188
+ */
189
+ export function buildRelayBody({
190
+ input,
191
+ message,
192
+ parameters,
193
+ threadId,
194
+ }: {
195
+ input: Record<string, unknown>;
196
+ message?: string;
197
+ parameters?: AgentCallParams;
198
+ threadId?: string;
199
+ }): AgentCallBody | string {
200
+ let messages: AgentCallMessage[];
201
+ if (message !== undefined) {
202
+ messages = [{ role: "user", content: message }];
203
+ } else if (isMessageList(input.messages)) {
204
+ messages = input.messages;
205
+ } else {
206
+ return "a connected agent takes a conversation: give `message`, or `input` with a `messages` list.";
207
+ }
208
+ const body: AgentCallBody = { messages };
209
+ const thread = threadId ?? (typeof input.threadId === "string" ? input.threadId : undefined);
210
+ if (thread) body.threadId = thread;
211
+ if (isMessageList(input.newMessages)) body.newMessages = input.newMessages;
212
+ if (input.session !== undefined) body.session = input.session;
213
+ let inputParams: AgentCallParams | undefined;
214
+ if (input.params !== undefined) {
215
+ const parsed = agentCallParamsSchema.safeParse(input.params);
216
+ if (!parsed.success) {
217
+ return "`params` takes one flat object of string, number or boolean values. Nested objects and arrays are not run parameters.";
218
+ }
219
+ inputParams = parsed.data;
220
+ }
221
+ if (inputParams || parameters) body.params = { ...(inputParams ?? {}), ...(parameters ?? {}) };
222
+ return body;
223
+ }
224
+
225
+ /**
226
+ * Run an agent. A connected agent runs one turn through the relay; an HTTP
227
+ * agent is called at its URL; a workflow-linked agent runs on the workflow
228
+ * engine.
66
229
  */
67
- export async function runAgent(
68
- id: string,
69
- input?: Record<string, unknown>,
70
- ): Promise<{ agentType: string; result: Record<string, unknown> }> {
230
+ export async function runAgent({
231
+ id,
232
+ input,
233
+ message,
234
+ parameters,
235
+ threadId,
236
+ }: {
237
+ id: string;
238
+ input?: Record<string, unknown>;
239
+ message?: string;
240
+ parameters?: AgentCallParams;
241
+ threadId?: string;
242
+ }): Promise<{ agentType: string; result: Record<string, unknown> }> {
71
243
  const agent = await getAgent(id);
72
244
  const config = agent.config ?? {};
73
245
 
246
+ if (agent.type === "connected") {
247
+ const body = buildRelayBody({ input: input ?? {}, message, parameters, threadId });
248
+ if (typeof body === "string") throw new Error(body);
249
+ if (agent.status === "offline") {
250
+ throw new Error(
251
+ `Agent "${agent.name}" is offline. Start the process that calls connectAgent (or connect_agent) and try again.`,
252
+ );
253
+ }
254
+ const result = await callAgent({ id: agent.id, body });
255
+ return { agentType: "connected", result: result as unknown as Record<string, unknown> };
256
+ }
257
+
74
258
  if (agent.type === "http") {
75
259
  const url = (config as Record<string, unknown>).url as string | undefined;
76
260
  if (!url) {
@@ -91,7 +275,7 @@ export async function runAgent(
91
275
  if (!workflowId) {
92
276
  throw new Error(
93
277
  `Agent "${agent.name}" (type: ${agent.type}) cannot be executed directly. ` +
94
- `Only HTTP agents and workflow-linked agents can be run.`,
278
+ `Only connected agents, HTTP agents and workflow-linked agents can be run.`,
95
279
  );
96
280
  }
97
281
 
@@ -103,6 +287,6 @@ export async function runAgent(
103
287
  export async function deleteAgent(id: string): Promise<{ id: string; name: string }> {
104
288
  return makeRequest(
105
289
  "DELETE",
106
- `/api/agents/${encodeURIComponent(id)}`,
290
+ `/api/v1/agents/${encodeURIComponent(id)}`,
107
291
  ) as Promise<{ id: string; name: string }>;
108
292
  }
@@ -0,0 +1,157 @@
1
+ import type { z } from "zod";
2
+ import { makeRequest } from "./langwatch-api.js";
3
+ import type {
4
+ runParametersSchema,
5
+ runPlanScopeSchema,
6
+ runPlanTargetSchema,
7
+ RunPlanTargetWire,
8
+ } from "./schemas/run-plan.js";
9
+ import type { EvaluatorAttachmentWire } from "./schemas/suite-fields.js";
10
+
11
+ /**
12
+ * Client for `/api/v1/run-plans`.
13
+ *
14
+ * A run plan is what you run, and its NAME identifies it: running against a
15
+ * name that exists replaces that plan's configuration, running against a new
16
+ * name creates one. Configuration is the scope, the targets, the repeat count
17
+ * and the two models. Parameters, the note and the idempotency key belong to
18
+ * one run, not to the plan.
19
+ */
20
+
21
+ /**
22
+ * The shapes the run plan tools accept, taken from the zod that validates
23
+ * them. They are inferred rather than written twice, so the schema stays the
24
+ * one place the shape is stated.
25
+ */
26
+
27
+ /** What a run plan covers. Mirrors `suiteScopeSchema` on the platform. */
28
+ export type RunPlanScope = z.infer<typeof runPlanScopeSchema>;
29
+
30
+ /** One thing a plan runs its scenarios against, as a tool caller writes it. */
31
+ export type RunPlanTarget = z.infer<typeof runPlanTargetSchema>;
32
+
33
+ /** The values a run supplies for the parameters its scenarios declare. */
34
+ export type RunParameters = z.infer<typeof runParametersSchema>;
35
+
36
+ export interface RunPlan {
37
+ id: string;
38
+ name: string;
39
+ slug: string;
40
+ scope: RunPlanScope | null;
41
+ scenarioIds: string[];
42
+ targets: RunPlanTargetWire[];
43
+ repeatCount: number;
44
+ simulatorModel: string | null;
45
+ judgeModel: string | null;
46
+ labels: string[];
47
+ archivedAt: string | null;
48
+ createdAt: string;
49
+ updatedAt: string;
50
+ platformUrl: string;
51
+ /** The plan's own evaluators. Absent on servers that predate them. */
52
+ evaluators?: EvaluatorAttachmentWire[];
53
+ }
54
+
55
+ /** The configuration a run either writes onto a plan or creates it with. */
56
+ export interface RunPlanConfig {
57
+ scope: RunPlanScope;
58
+ targets: RunPlanTargetWire[];
59
+ repeatCount?: number;
60
+ simulatorModel?: string | null;
61
+ judgeModel?: string | null;
62
+ scenarioIds?: string[];
63
+ /**
64
+ * The plan's own evaluators, run beside the ones its test suites attach. A
65
+ * plan evaluator reads the conversation and the trace, never a scenario
66
+ * field. Left out, the plan keeps what it already holds.
67
+ */
68
+ evaluators?: EvaluatorAttachmentWire[];
69
+ }
70
+
71
+ export interface RunPlanRunResult {
72
+ scheduled: boolean;
73
+ batchRunId: string;
74
+ setId: string;
75
+ jobCount: number;
76
+ skippedArchived: {
77
+ scenarios: string[];
78
+ targets: string[];
79
+ };
80
+ items: Array<{
81
+ scenarioRunId: string;
82
+ scenarioId: string;
83
+ target: RunPlanTargetWire;
84
+ name: string | null;
85
+ }>;
86
+ runPlanId: string;
87
+ planName: string;
88
+ /** True when this run created the plan, false when it joined an existing one. */
89
+ created: boolean;
90
+ platformUrl: string;
91
+ }
92
+
93
+ export interface RunPlanArchiveResponse {
94
+ id: string;
95
+ archived: boolean;
96
+ }
97
+
98
+ /** Lists the run plans of the project. */
99
+ export async function listRunPlans(params?: {
100
+ includeArchived?: boolean;
101
+ }): Promise<RunPlan[]> {
102
+ const query = params?.includeArchived ? "?includeArchived=true" : "";
103
+ return makeRequest("GET", `/api/v1/run-plans${query}`) as Promise<RunPlan[]>;
104
+ }
105
+
106
+ /** Retrieves a single run plan by id. */
107
+ export async function getRunPlan(id: string): Promise<RunPlan> {
108
+ return makeRequest(
109
+ "GET",
110
+ `/api/v1/run-plans/${encodeURIComponent(id)}`,
111
+ ) as Promise<RunPlan>;
112
+ }
113
+
114
+ /**
115
+ * Runs a configuration by plan name. A name that exists is replaced with this
116
+ * configuration, a name that does not is created. Sending no name lets the
117
+ * server derive one.
118
+ */
119
+ export async function runRunPlan(data: {
120
+ name?: string;
121
+ config: RunPlanConfig;
122
+ idempotencyKey?: string;
123
+ parameters?: RunParameters;
124
+ note?: string;
125
+ }): Promise<RunPlanRunResult> {
126
+ return makeRequest(
127
+ "POST",
128
+ "/api/v1/run-plans/run",
129
+ data,
130
+ ) as Promise<RunPlanRunResult>;
131
+ }
132
+
133
+ /** Runs a plan again with the configuration it already holds. */
134
+ export async function rerunRunPlan(
135
+ id: string,
136
+ data?: {
137
+ idempotencyKey?: string;
138
+ parameters?: RunParameters;
139
+ note?: string;
140
+ },
141
+ ): Promise<RunPlanRunResult> {
142
+ return makeRequest(
143
+ "POST",
144
+ `/api/v1/run-plans/${encodeURIComponent(id)}/run`,
145
+ data ?? {},
146
+ ) as Promise<RunPlanRunResult>;
147
+ }
148
+
149
+ /** Archives a run plan. */
150
+ export async function archiveRunPlan(
151
+ id: string,
152
+ ): Promise<RunPlanArchiveResponse> {
153
+ return makeRequest(
154
+ "DELETE",
155
+ `/api/v1/run-plans/${encodeURIComponent(id)}`,
156
+ ) as Promise<RunPlanArchiveResponse>;
157
+ }
@@ -1,4 +1,5 @@
1
1
  import { makeRequest } from "./langwatch-api.js";
2
+ import type { ScenarioFieldValues } from "./schemas/suite-fields.js";
2
3
 
3
4
  // --- Scenario types ---
4
5
 
@@ -8,6 +9,10 @@ export interface ScenarioSummary {
8
9
  situation: string;
9
10
  criteria: string[];
10
11
  labels: string[];
12
+ /** The test suite this scenario is filed in, null when it is unfiled. */
13
+ testSuiteId?: string | null;
14
+ /** The scenario's value for each field its test suite declares. */
15
+ fields?: ScenarioFieldValues;
11
16
  }
12
17
 
13
18
  export interface ScenarioArchiveResponse {
@@ -36,6 +41,8 @@ export async function createScenario(data: {
36
41
  situation: string;
37
42
  criteria?: string[];
38
43
  labels?: string[];
44
+ testSuiteId?: string | null;
45
+ fields?: ScenarioFieldValues;
39
46
  }): Promise<ScenarioSummary> {
40
47
  return makeRequest("POST", "/api/scenarios", data) as Promise<ScenarioSummary>;
41
48
  }
@@ -47,6 +54,8 @@ export async function updateScenario(params: {
47
54
  situation?: string;
48
55
  criteria?: string[];
49
56
  labels?: string[];
57
+ testSuiteId?: string | null;
58
+ fields?: ScenarioFieldValues;
50
59
  }): Promise<ScenarioSummary> {
51
60
  const { id, ...data } = params;
52
61
  return makeRequest(
@@ -1,4 +1,5 @@
1
1
  import { makeRequest } from "./langwatch-api.js";
2
+ import type { SimulationRunEvaluation } from "./tools/format-suite-details.js";
2
3
 
3
4
  export interface SimulationRunSummary {
4
5
  scenarioRunId: string;
@@ -14,8 +15,12 @@ export interface SimulationRunSummary {
14
15
  metCriteria?: string[];
15
16
  unmetCriteria?: string[];
16
17
  error?: string | null;
18
+ /** One result per evaluator that ran after the conversation. */
19
+ evaluations?: SimulationRunEvaluation[];
17
20
  } | null;
18
21
  messages?: Array<{ role: string; content: string }>;
22
+ /** Why the run was started, as given when it was queued. Null without one. */
23
+ note?: string | null;
19
24
  timestamp: number;
20
25
  updatedAt: number;
21
26
  }
@@ -0,0 +1,135 @@
1
+ import { makeRequest } from "./langwatch-api.js";
2
+ import type {
3
+ RunParameters,
4
+ RunPlanRunResult,
5
+ } from "./langwatch-api-run-plans.js";
6
+ import type { RunPlanTargetWire } from "./schemas/run-plan.js";
7
+ import type {
8
+ EvaluatorAttachmentWire,
9
+ SuiteField,
10
+ } from "./schemas/suite-fields.js";
11
+
12
+ /**
13
+ * Client for `/api/v1/test-suites`.
14
+ *
15
+ * A test suite groups scenarios: a name and the scenarios filed in it.
16
+ * Running one is sugar over a run plan, so the run returns the same result a
17
+ * run plan does.
18
+ */
19
+
20
+ export interface TestSuite {
21
+ id: string;
22
+ name: string;
23
+ slug: string;
24
+ scenarioIds: string[];
25
+ scenarioCount: number;
26
+ archivedAt: string | null;
27
+ createdAt: string;
28
+ updatedAt: string;
29
+ platformUrl: string;
30
+ /** The fields the suite declares. Absent on servers that predate them. */
31
+ fields?: SuiteField[];
32
+ /** The evaluators attached to the suite. Absent on servers that predate them. */
33
+ evaluators?: EvaluatorAttachmentWire[];
34
+ }
35
+
36
+ export interface TestSuiteDetail extends TestSuite {
37
+ scenarios: Array<{ id: string; name: string }>;
38
+ }
39
+
40
+ export interface TestSuiteArchiveResponse {
41
+ id: string;
42
+ archived: boolean;
43
+ }
44
+
45
+ /** Lists the test suites of the project. */
46
+ export async function listTestSuites(): Promise<TestSuite[]> {
47
+ return makeRequest("GET", "/api/v1/test-suites") as Promise<TestSuite[]>;
48
+ }
49
+
50
+ /** Creates a test suite, with the fields and the evaluators it declares. */
51
+ export async function createTestSuite(data: {
52
+ name: string;
53
+ fields?: SuiteField[];
54
+ evaluators?: EvaluatorAttachmentWire[];
55
+ }): Promise<TestSuite> {
56
+ return makeRequest(
57
+ "POST",
58
+ "/api/v1/test-suites",
59
+ data,
60
+ ) as Promise<TestSuite>;
61
+ }
62
+
63
+ /** Retrieves a test suite with the scenarios filed in it. */
64
+ export async function getTestSuite(id: string): Promise<TestSuiteDetail> {
65
+ return makeRequest(
66
+ "GET",
67
+ `/api/v1/test-suites/${encodeURIComponent(id)}`,
68
+ ) as Promise<TestSuiteDetail>;
69
+ }
70
+
71
+ /**
72
+ * Edits a test suite: any of its name, its fields and its evaluators. A list
73
+ * given here replaces the one the suite holds.
74
+ */
75
+ export async function updateTestSuite(params: {
76
+ id: string;
77
+ name?: string;
78
+ fields?: SuiteField[];
79
+ evaluators?: EvaluatorAttachmentWire[];
80
+ }): Promise<TestSuite> {
81
+ const { id, ...data } = params;
82
+ return makeRequest(
83
+ "PATCH",
84
+ `/api/v1/test-suites/${encodeURIComponent(id)}`,
85
+ data,
86
+ ) as Promise<TestSuite>;
87
+ }
88
+
89
+ /** Renames a test suite. */
90
+ export async function renameTestSuite(params: {
91
+ id: string;
92
+ name: string;
93
+ }): Promise<TestSuite> {
94
+ return makeRequest(
95
+ "PATCH",
96
+ `/api/v1/test-suites/${encodeURIComponent(params.id)}`,
97
+ { name: params.name },
98
+ ) as Promise<TestSuite>;
99
+ }
100
+
101
+ /** Archives a test suite and the scenarios filed in it. */
102
+ export async function archiveTestSuite(
103
+ id: string,
104
+ ): Promise<TestSuiteArchiveResponse> {
105
+ return makeRequest(
106
+ "DELETE",
107
+ `/api/v1/test-suites/${encodeURIComponent(id)}`,
108
+ ) as Promise<TestSuiteArchiveResponse>;
109
+ }
110
+
111
+ /**
112
+ * Runs every scenario of a test suite against the given targets. The server
113
+ * creates or joins the run plan named "<suite name> <target name>" when no
114
+ * name is sent.
115
+ */
116
+ export async function runTestSuite({
117
+ id,
118
+ ...data
119
+ }: {
120
+ id: string;
121
+ targets: RunPlanTargetWire[];
122
+ name?: string;
123
+ repeatCount?: number;
124
+ simulatorModel?: string | null;
125
+ judgeModel?: string | null;
126
+ parameters?: RunParameters;
127
+ note?: string;
128
+ idempotencyKey?: string;
129
+ }): Promise<RunPlanRunResult> {
130
+ return makeRequest(
131
+ "POST",
132
+ `/api/v1/test-suites/${encodeURIComponent(id)}/run`,
133
+ data,
134
+ ) as Promise<RunPlanRunResult>;
135
+ }