@dypai-ai/mcp 1.2.4 → 1.3.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.
- package/package.json +1 -1
- package/src/index.js +525 -14
- package/src/tools/frontend.js +37 -11
- package/src/tools/scaffold.js +33 -3
- package/src/tools/sync/codec.js +93 -7
- package/src/tools/sync/planner.js +6 -0
- package/src/tools/sync/pull.js +363 -3
- package/src/tools/sync/test-endpoint.js +68 -2
- package/src/tools/sync/validate.js +300 -15
- package/src/tools/sync.js +133 -0
- package/src/tools/trace-summarize.js +8 -0
package/src/tools/sync/pull.js
CHANGED
|
@@ -105,6 +105,339 @@ Declarative snapshot of your DYPAI project's backend.
|
|
|
105
105
|
|
|
106
106
|
Paths inside YAML (e.g. \`query_file: sql/create_invoice.sql\`) are always relative
|
|
107
107
|
to this folder's root, regardless of where the YAML lives.
|
|
108
|
+
|
|
109
|
+
## Reference examples
|
|
110
|
+
|
|
111
|
+
When the project has no endpoints yet, \`dypai_pull\` writes three reference
|
|
112
|
+
files (all with the \`.disabled\` suffix so \`dypai_push\` ignores them):
|
|
113
|
+
|
|
114
|
+
- \`endpoints/_example.yaml.disabled\` full workflow tour
|
|
115
|
+
- \`endpoints/_example-tool.yaml.disabled\` endpoint marked \`tool: true\`
|
|
116
|
+
- \`endpoints/_example-agent.yaml.disabled\` endpoint with an agent node using the tool
|
|
117
|
+
|
|
118
|
+
Read them to learn the canonical YAML format (placeholders, branching, error
|
|
119
|
+
handling, agent tools). Copy + adapt:
|
|
120
|
+
|
|
121
|
+
cp endpoints/_example.yaml.disabled endpoints/my-feature.yaml
|
|
122
|
+
cp endpoints/_example-tool.yaml.disabled endpoints/search-products.yaml
|
|
123
|
+
cp endpoints/_example-agent.yaml.disabled endpoints/chat.yaml
|
|
124
|
+
`
|
|
125
|
+
|
|
126
|
+
// Reference endpoint that ships with empty projects so the agent can learn
|
|
127
|
+
// the YAML format by reading a real, complete example. Covers: trigger, input
|
|
128
|
+
// schema, output schema, multi-node workflow, ${input.x} / ${nodes.x.y} /
|
|
129
|
+
// ${current_user_id} placeholders, expressions, branching with `handle`,
|
|
130
|
+
// per-node error handling, multiple return paths, *_file references.
|
|
131
|
+
const EXAMPLE_ENDPOINT_YAML = `# ============================================================================
|
|
132
|
+
# DYPAI endpoint reference — complete tour of the YAML format.
|
|
133
|
+
# This file is IGNORED by dypai_push, dypai_validate, dypai_diff and
|
|
134
|
+
# dypai_test_endpoint (the .disabled extension keeps it out of every flow).
|
|
135
|
+
#
|
|
136
|
+
# Copy + adapt when authoring a new endpoint:
|
|
137
|
+
# cp endpoints/_example.yaml.disabled endpoints/my-feature.yaml
|
|
138
|
+
# Then edit and remove the .disabled suffix.
|
|
139
|
+
# ============================================================================
|
|
140
|
+
|
|
141
|
+
name: example-process-order
|
|
142
|
+
description: |
|
|
143
|
+
Reference endpoint demonstrating the canonical YAML format:
|
|
144
|
+
trigger, input/output schemas, multi-node workflow, placeholders,
|
|
145
|
+
branching, per-node error handling, and multiple return paths.
|
|
146
|
+
method: POST
|
|
147
|
+
|
|
148
|
+
# ─── Trigger ────────────────────────────────────────────────────────────────
|
|
149
|
+
# Pick exactly one. Options:
|
|
150
|
+
# trigger: { http_api: { auth_mode: jwt | api_key | public } }
|
|
151
|
+
# trigger: { webhook: {} } # path is auto-generated
|
|
152
|
+
# trigger: { schedule: { cron: "0 9 * * 1-5", timezone: Europe/Madrid } }
|
|
153
|
+
# trigger: { telegram: {} } # incoming Telegram messages
|
|
154
|
+
#
|
|
155
|
+
# auth_mode notes:
|
|
156
|
+
# jwt — user-authenticated. \${current_user_id} / \${current_user_role} available.
|
|
157
|
+
# api_key — server-to-server. No user context.
|
|
158
|
+
# public — anonymous. Use only for read-only, non-sensitive data.
|
|
159
|
+
trigger:
|
|
160
|
+
http_api:
|
|
161
|
+
auth_mode: jwt
|
|
162
|
+
|
|
163
|
+
# ─── Input schema (JSON Schema) ─────────────────────────────────────────────
|
|
164
|
+
# Validates the request body. Fields here are referenced via \${input.<field>}.
|
|
165
|
+
# The validator (dypai_validate) catches typos in placeholders against this.
|
|
166
|
+
input:
|
|
167
|
+
type: object
|
|
168
|
+
required: [product_id, quantity]
|
|
169
|
+
properties:
|
|
170
|
+
product_id:
|
|
171
|
+
type: string
|
|
172
|
+
format: uuid
|
|
173
|
+
quantity:
|
|
174
|
+
type: integer
|
|
175
|
+
minimum: 1
|
|
176
|
+
note:
|
|
177
|
+
type: string
|
|
178
|
+
|
|
179
|
+
# ─── Output schema (optional but recommended) ───────────────────────────────
|
|
180
|
+
# Describes the response shape. Helps the validator + frontend type-checkers.
|
|
181
|
+
output:
|
|
182
|
+
type: object
|
|
183
|
+
properties:
|
|
184
|
+
order_id: { type: string }
|
|
185
|
+
total: { type: number }
|
|
186
|
+
status: { type: string }
|
|
187
|
+
|
|
188
|
+
# ─── Workflow ───────────────────────────────────────────────────────────────
|
|
189
|
+
# Nodes are the steps. Placeholders wire data flow between them:
|
|
190
|
+
#
|
|
191
|
+
# \${input.<field>} — the request body / query params
|
|
192
|
+
# \${nodes.<id>.<field>} — output of a previous node
|
|
193
|
+
# \${current_user_id} — UUID of the JWT-authenticated user
|
|
194
|
+
# \${current_user_role} — role name from the JWT
|
|
195
|
+
#
|
|
196
|
+
# Expressions inside placeholders work: arithmetic, comparisons, JS-ish.
|
|
197
|
+
# \${input.qty * 2}, \${nodes.x.stock >= input.qty}
|
|
198
|
+
#
|
|
199
|
+
# In SQL contexts (custom_query, dypai_database queries) the engine
|
|
200
|
+
# automatically casts placeholders by value shape:
|
|
201
|
+
# UUID-shaped string → binds as ::uuid
|
|
202
|
+
# plain object/array → binds as ::jsonb
|
|
203
|
+
# Date instance → binds as ::timestamptz
|
|
204
|
+
# text / int / bool → binds without an explicit cast (Postgres infers)
|
|
205
|
+
# So you write SQL naturally — DON'T add '\${current_user_id}'::uuid manually.
|
|
206
|
+
# Just write: WHERE user_id = \${current_user_id}
|
|
207
|
+
workflow:
|
|
208
|
+
nodes:
|
|
209
|
+
|
|
210
|
+
# 1. Read the product. \`operation: query\` for ANY read — pure SQL.
|
|
211
|
+
- id: get_product
|
|
212
|
+
type: dypai_database
|
|
213
|
+
operation: query
|
|
214
|
+
query: SELECT * FROM products WHERE id = \${input.product_id} LIMIT 1
|
|
215
|
+
# Output is accessible as \${nodes.get_product.<column>} (first row's columns)
|
|
216
|
+
|
|
217
|
+
# 2. Branch on stock availability.
|
|
218
|
+
# \`logic\` nodes with \`operation: if\` send flow down two edges
|
|
219
|
+
# differentiated by \`handle: "true"\` and \`handle: "false"\`.
|
|
220
|
+
- id: check_stock
|
|
221
|
+
type: logic
|
|
222
|
+
operation: if
|
|
223
|
+
condition: \${nodes.get_product.stock >= input.quantity}
|
|
224
|
+
|
|
225
|
+
# 3. Insert the order. \`operation: mutation\` for trivial single-table writes
|
|
226
|
+
# — declarative, no SQL needed. The shape (insert/update/delete) decides.
|
|
227
|
+
- id: insert_order
|
|
228
|
+
type: dypai_database
|
|
229
|
+
operation: mutation
|
|
230
|
+
table: orders
|
|
231
|
+
insert:
|
|
232
|
+
user_id: \${current_user_id} # JWT identity
|
|
233
|
+
product_id: \${input.product_id} # request input
|
|
234
|
+
quantity: \${input.quantity}
|
|
235
|
+
unit_price: \${nodes.get_product.price} # cross-node ref
|
|
236
|
+
total: \${nodes.get_product.price * input.quantity} # expression
|
|
237
|
+
note: \${input.note}
|
|
238
|
+
returning: "*"
|
|
239
|
+
# For UPSERT: add on_conflict: { columns: [id], action: update }
|
|
240
|
+
# For UPDATE: replace \`insert:\` with \`update: {...}\` + \`where: {...}\`
|
|
241
|
+
# For DELETE: replace \`insert:\` with \`delete: true\` + \`where: {...}\`
|
|
242
|
+
#
|
|
243
|
+
# Long SQL (>500 chars) inside \`operation: query\` is auto-extracted on
|
|
244
|
+
# \`dypai_pull\` to sql/<endpoint>.sql and referenced like:
|
|
245
|
+
# query_file: sql/insert_order.sql
|
|
246
|
+
# Same for prompts (system_prompt_file) and JS/Python code (code_file).
|
|
247
|
+
|
|
248
|
+
# 4. Notify a downstream system. \`on_error_step_id\` routes failures
|
|
249
|
+
# to \`build_response\` so a notification outage doesn't 500 the order.
|
|
250
|
+
- id: notify
|
|
251
|
+
type: http_request
|
|
252
|
+
method: POST
|
|
253
|
+
url: https://hooks.example.com/orders
|
|
254
|
+
body:
|
|
255
|
+
order_id: \${nodes.insert_order.id}
|
|
256
|
+
total: \${nodes.insert_order.total}
|
|
257
|
+
on_error_step_id: build_response
|
|
258
|
+
|
|
259
|
+
# 5. Build the success response. Exactly one node per code path needs
|
|
260
|
+
# \`return: true\` — it marks that node's output as the HTTP body.
|
|
261
|
+
- id: build_response
|
|
262
|
+
type: set_fields
|
|
263
|
+
return: true
|
|
264
|
+
fields:
|
|
265
|
+
order_id: \${nodes.insert_order.id}
|
|
266
|
+
total: \${nodes.insert_order.total}
|
|
267
|
+
status: created
|
|
268
|
+
|
|
269
|
+
# 6. Error path: out of stock. Different shape, also \`return: true\`.
|
|
270
|
+
# Multiple return nodes are fine as long as only one runs per execution.
|
|
271
|
+
- id: out_of_stock
|
|
272
|
+
type: set_fields
|
|
273
|
+
return: true
|
|
274
|
+
fields:
|
|
275
|
+
error: out_of_stock
|
|
276
|
+
message: Not enough stock for the requested quantity
|
|
277
|
+
available: \${nodes.get_product.stock}
|
|
278
|
+
requested: \${input.quantity}
|
|
279
|
+
|
|
280
|
+
# ─── Edges ────────────────────────────────────────────────────────────────
|
|
281
|
+
# Connect nodes. Form: { from: <id>, to: <id> }.
|
|
282
|
+
# For \`logic\` if-nodes use \`handle: "true"\` / \`handle: "false"\` to pick the branch.
|
|
283
|
+
edges:
|
|
284
|
+
- { from: get_product, to: check_stock }
|
|
285
|
+
- { from: check_stock, to: insert_order, handle: "true" }
|
|
286
|
+
- { from: check_stock, to: out_of_stock, handle: "false" }
|
|
287
|
+
- { from: insert_order, to: notify }
|
|
288
|
+
- { from: notify, to: build_response }
|
|
289
|
+
|
|
290
|
+
# \`on_error\` at the workflow level: stop (default) | continue
|
|
291
|
+
# on_error: stop
|
|
292
|
+
`
|
|
293
|
+
|
|
294
|
+
// Reference TOOL endpoint — an endpoint marked \`tool: true\` so an \`agent\` node
|
|
295
|
+
// elsewhere can invoke it. Pair with EXAMPLE_AGENT_ENDPOINT_YAML below.
|
|
296
|
+
const EXAMPLE_TOOL_ENDPOINT_YAML = `# ============================================================================
|
|
297
|
+
# DYPAI reference — endpoint exposed as a TOOL for \`agent\` nodes.
|
|
298
|
+
# Ignored by dypai_push/validate/diff/test_endpoint (.disabled suffix).
|
|
299
|
+
#
|
|
300
|
+
# Pair with _example-agent.yaml.disabled — that one has an agent node that
|
|
301
|
+
# references THIS endpoint by name.
|
|
302
|
+
#
|
|
303
|
+
# Copy + adapt:
|
|
304
|
+
# cp endpoints/_example-tool.yaml.disabled endpoints/search-products.yaml
|
|
305
|
+
# ============================================================================
|
|
306
|
+
|
|
307
|
+
name: example-search-products
|
|
308
|
+
description: Search the product catalog. Exposed as a tool for agent nodes.
|
|
309
|
+
method: GET
|
|
310
|
+
|
|
311
|
+
# ─── Tool flag + description ────────────────────────────────────────────────
|
|
312
|
+
# \`tool: true\` exposes this endpoint to agent nodes (writes is_tool=true in DB).
|
|
313
|
+
# \`tool_description\` is the prompt the LLM reads when deciding to call it.
|
|
314
|
+
# Be SPECIFIC about when to use it and what it returns — vague descriptions
|
|
315
|
+
# cause the LLM to skip useful tools or misuse them.
|
|
316
|
+
tool: true
|
|
317
|
+
tool_description: |
|
|
318
|
+
Search the product catalog by free-text query.
|
|
319
|
+
Use when the user asks about availability, prices, or categories.
|
|
320
|
+
Returns up to \`limit\` products with id, name, price, stock.
|
|
321
|
+
|
|
322
|
+
# ─── Input schema (tight JSON Schema with descriptions) ─────────────────────
|
|
323
|
+
# The LLM uses THIS schema to fabricate tool arguments. Every field benefits
|
|
324
|
+
# from a \`description\`; typed and required fields keep hallucinated args out.
|
|
325
|
+
input:
|
|
326
|
+
type: object
|
|
327
|
+
required: [query]
|
|
328
|
+
properties:
|
|
329
|
+
query:
|
|
330
|
+
type: string
|
|
331
|
+
description: Free-text search term (matches product name, case-insensitive).
|
|
332
|
+
limit:
|
|
333
|
+
type: integer
|
|
334
|
+
default: 10
|
|
335
|
+
minimum: 1
|
|
336
|
+
maximum: 50
|
|
337
|
+
description: Max results to return.
|
|
338
|
+
|
|
339
|
+
# ─── Output schema (optional but recommended for tools) ─────────────────────
|
|
340
|
+
output:
|
|
341
|
+
type: object
|
|
342
|
+
properties:
|
|
343
|
+
results:
|
|
344
|
+
type: array
|
|
345
|
+
items:
|
|
346
|
+
type: object
|
|
347
|
+
properties:
|
|
348
|
+
id: { type: string }
|
|
349
|
+
name: { type: string }
|
|
350
|
+
price: { type: number }
|
|
351
|
+
stock: { type: integer }
|
|
352
|
+
|
|
353
|
+
# ─── Trigger ────────────────────────────────────────────────────────────────
|
|
354
|
+
# Agent calls are server-to-server → \`api_key\`. Use \`jwt\` only if the tool
|
|
355
|
+
# needs \${current_user_id}. NEVER \`public\` for a tool that writes data.
|
|
356
|
+
trigger:
|
|
357
|
+
http_api:
|
|
358
|
+
auth_mode: api_key
|
|
359
|
+
|
|
360
|
+
workflow:
|
|
361
|
+
nodes:
|
|
362
|
+
- id: run
|
|
363
|
+
type: dypai_database
|
|
364
|
+
operation: query
|
|
365
|
+
return: true
|
|
366
|
+
query: |
|
|
367
|
+
SELECT id, name, price, stock
|
|
368
|
+
FROM products
|
|
369
|
+
WHERE name ILIKE '%' || \${input.query} || '%'
|
|
370
|
+
ORDER BY name
|
|
371
|
+
LIMIT \${input.limit}
|
|
372
|
+
`
|
|
373
|
+
|
|
374
|
+
// Reference endpoint with an \`agent\` node that USES a tool endpoint by name.
|
|
375
|
+
// Pair with EXAMPLE_TOOL_ENDPOINT_YAML above.
|
|
376
|
+
const EXAMPLE_AGENT_ENDPOINT_YAML = `# ============================================================================
|
|
377
|
+
# DYPAI reference — endpoint with an \`agent\` node that invokes tool endpoints.
|
|
378
|
+
# Ignored by dypai_push/validate/diff/test_endpoint (.disabled suffix).
|
|
379
|
+
#
|
|
380
|
+
# Pair with _example-tool.yaml.disabled — this file references that one by name.
|
|
381
|
+
#
|
|
382
|
+
# Copy + adapt:
|
|
383
|
+
# cp endpoints/_example-agent.yaml.disabled endpoints/chat.yaml
|
|
384
|
+
# ============================================================================
|
|
385
|
+
|
|
386
|
+
name: example-chat-assistant
|
|
387
|
+
description: Chat assistant that uses tool endpoints to answer questions about products.
|
|
388
|
+
method: POST
|
|
389
|
+
|
|
390
|
+
trigger:
|
|
391
|
+
http_api:
|
|
392
|
+
auth_mode: jwt # user-authenticated chat
|
|
393
|
+
|
|
394
|
+
input:
|
|
395
|
+
type: object
|
|
396
|
+
required: [message]
|
|
397
|
+
properties:
|
|
398
|
+
message:
|
|
399
|
+
type: string
|
|
400
|
+
description: The user's message to the assistant.
|
|
401
|
+
|
|
402
|
+
output:
|
|
403
|
+
type: object
|
|
404
|
+
properties:
|
|
405
|
+
reply: { type: string }
|
|
406
|
+
tool_calls: { type: array }
|
|
407
|
+
|
|
408
|
+
workflow:
|
|
409
|
+
nodes:
|
|
410
|
+
- id: chat
|
|
411
|
+
type: agent
|
|
412
|
+
provider: openai # openai | anthropic | google
|
|
413
|
+
model: gpt-4o # model id — see list via UI or search_docs
|
|
414
|
+
credential: openai-prod # credential NAME — must exist (get_app_credentials)
|
|
415
|
+
return: true
|
|
416
|
+
|
|
417
|
+
system_prompt: |
|
|
418
|
+
You are a helpful shop assistant. You have access to tools for searching
|
|
419
|
+
the product catalog and sending emails. When the user asks about
|
|
420
|
+
availability, prices, or categories, call example-search-products.
|
|
421
|
+
Always be concise and friendly.
|
|
422
|
+
|
|
423
|
+
input: \${input.message} # what the user said → the agent's turn
|
|
424
|
+
|
|
425
|
+
# ─── Tools (IMPORTANT — BY NAME, not UUID) ─────────────────────────────
|
|
426
|
+
# In YAML the parameter is \`tools: [<endpoint_name>, ...]\`. The codec
|
|
427
|
+
# resolves names ↔ UUIDs automatically on push/pull. Writing
|
|
428
|
+
# \`tool_ids: [...]\` in YAML BYPASSES the codec and fails in production.
|
|
429
|
+
#
|
|
430
|
+
# Every name here MUST reference an endpoint with \`tool: true\`.
|
|
431
|
+
# dypai_validate catches typos (rule: agent_tool_not_found) and lists
|
|
432
|
+
# the available tool endpoints in its fix_hint.
|
|
433
|
+
tools:
|
|
434
|
+
- example-search-products # ← references the other example file
|
|
435
|
+
# - example-send-email # add more tools as you define them
|
|
436
|
+
|
|
437
|
+
# ─── Agent behavior tuning ─────────────────────────────────────────────
|
|
438
|
+
max_iterations: 5 # max rounds of tool-calling (anti-runaway)
|
|
439
|
+
temperature: 0.7
|
|
440
|
+
tool_timeout: 30 # seconds per tool call
|
|
108
441
|
`
|
|
109
442
|
|
|
110
443
|
async function execSql(projectId, sql) {
|
|
@@ -150,10 +483,13 @@ function renderYaml(doc) {
|
|
|
150
483
|
export const dypaiPullTool = {
|
|
151
484
|
name: "dypai_pull",
|
|
152
485
|
description:
|
|
153
|
-
"
|
|
154
|
-
"Writes endpoints/<name>.yaml + sql/
|
|
486
|
+
"Downloads BACKEND state (endpoints, SQL, prompts, node catalog, realtime policies, schema) to local YAML files under ./dypai/. " +
|
|
487
|
+
"Writes endpoints/<name>.yaml + sql/ + prompts/ + code/ for extracted content. " +
|
|
155
488
|
"Canvas positions are stripped (regenerated by visual editor). Safe to run repeatedly. " +
|
|
156
|
-
"Use this to start editing a project locally with your editor + AI agent
|
|
489
|
+
"Use this to start editing a project locally with your editor + AI agent.\n\n" +
|
|
490
|
+
"SCOPE: backend only. This does NOT download frontend React/Vite source code — for that call " +
|
|
491
|
+
"`manage_frontend(operation: \"sync\", targetDirectory: <abs>)`. The two are independent; run both when " +
|
|
492
|
+
"starting fresh on a full-stack project.\n\n" +
|
|
157
493
|
"IMPORTANT: when called by an IDE-hosted MCP, the process cwd is often the user's home dir — " +
|
|
158
494
|
"pass an ABSOLUTE path in out_dir (e.g. /Users/me/projects/my-app/dypai) to avoid writing to the wrong place. " +
|
|
159
495
|
"If out_dir is relative, the tool tries to auto-detect the workspace via env vars and git markers, " +
|
|
@@ -321,6 +657,30 @@ export const dypaiPullTool = {
|
|
|
321
657
|
}
|
|
322
658
|
}
|
|
323
659
|
|
|
660
|
+
// Reference examples: only ship them on empty projects so the agent has
|
|
661
|
+
// canonical YAML tours to learn from. Once the project has real endpoints,
|
|
662
|
+
// those serve as the live examples — no need for the disabled stubs.
|
|
663
|
+
// .disabled suffix keeps them out of validate / diff / push / test_endpoint.
|
|
664
|
+
//
|
|
665
|
+
// Three separate files, each copy-ready for a different pattern:
|
|
666
|
+
// _example.yaml.disabled — full workflow tour (trigger, nodes, edges, branching)
|
|
667
|
+
// _example-tool.yaml.disabled — endpoint exposed as a tool for agent nodes
|
|
668
|
+
// _example-agent.yaml.disabled — endpoint with an agent node that uses the tool above
|
|
669
|
+
if (endpoints.length === 0) {
|
|
670
|
+
const refs = [
|
|
671
|
+
["endpoints/_example.yaml.disabled", EXAMPLE_ENDPOINT_YAML],
|
|
672
|
+
["endpoints/_example-tool.yaml.disabled", EXAMPLE_TOOL_ENDPOINT_YAML],
|
|
673
|
+
["endpoints/_example-agent.yaml.disabled", EXAMPLE_AGENT_ENDPOINT_YAML],
|
|
674
|
+
]
|
|
675
|
+
for (const [relPath, content] of refs) {
|
|
676
|
+
const fullPath = join(outDir, relPath)
|
|
677
|
+
try { await access(fullPath) } catch {
|
|
678
|
+
await writeFileEnsured(fullPath, content)
|
|
679
|
+
filesWritten.push(relPath)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
324
684
|
// Fetch project metadata to persist identity in the committed config
|
|
325
685
|
let projectInfo = null
|
|
326
686
|
if (project_id) {
|
|
@@ -15,6 +15,7 @@ import YAML from "yaml"
|
|
|
15
15
|
import { proxyToolCall } from "../proxy.js"
|
|
16
16
|
import { deserializeEndpoint } from "./codec.js"
|
|
17
17
|
import { readLocalConfig, fetchRemoteState } from "./planner.js"
|
|
18
|
+
import { runValidation } from "./validate.js"
|
|
18
19
|
import { summarizeTestWorkflowResponse } from "../trace-summarize.js"
|
|
19
20
|
|
|
20
21
|
// ─── Local endpoint file discovery ──────────────────────────────────────────
|
|
@@ -43,6 +44,9 @@ async function findEndpointByName(rootDir, name) {
|
|
|
43
44
|
const full = join(dir, e.name)
|
|
44
45
|
if (e.isDirectory()) await walk(full)
|
|
45
46
|
else if (e.isFile() && (e.name.endsWith(".yaml") || e.name.endsWith(".yml"))) {
|
|
47
|
+
// Skip disabled reference files (e.g. _example.yaml.disabled — already
|
|
48
|
+
// filtered by extension above, but also skip *.disabled.yaml form).
|
|
49
|
+
if (e.name.endsWith(".disabled.yaml") || e.name.endsWith(".disabled.yml")) continue
|
|
46
50
|
candidates.push(full)
|
|
47
51
|
}
|
|
48
52
|
}
|
|
@@ -109,11 +113,16 @@ export const dypaiTestEndpointTool = {
|
|
|
109
113
|
type: "string",
|
|
110
114
|
description: "Project UUID. Auto-resolved from dypai.config.yaml.",
|
|
111
115
|
},
|
|
116
|
+
skip_validation: {
|
|
117
|
+
type: "boolean",
|
|
118
|
+
description: "Skip the pre-flight validate pass. Default false — useful only when you intentionally want to send a malformed YAML to the engine to inspect its raw error.",
|
|
119
|
+
default: false,
|
|
120
|
+
},
|
|
112
121
|
},
|
|
113
122
|
required: ["endpoint"],
|
|
114
123
|
},
|
|
115
124
|
|
|
116
|
-
async execute({ endpoint, input = {}, as_user, trace_mode = "smart", root_dir = "./dypai", project_id } = {}) {
|
|
125
|
+
async execute({ endpoint, input = {}, as_user, trace_mode = "smart", root_dir = "./dypai", project_id, skip_validation = false } = {}) {
|
|
117
126
|
const rootDir = resolvePath(process.cwd(), root_dir)
|
|
118
127
|
|
|
119
128
|
if (!endpoint) {
|
|
@@ -172,6 +181,35 @@ export const dypaiTestEndpointTool = {
|
|
|
172
181
|
}
|
|
173
182
|
}
|
|
174
183
|
|
|
184
|
+
// ── Pre-flight validation (skippable) ────────────────────────────────────
|
|
185
|
+
// Run the same linter dypai_push uses, but filter to errors that affect THIS
|
|
186
|
+
// endpoint only — warnings and other-endpoint diagnostics shouldn't block
|
|
187
|
+
// a focused test. Catches things like mutation+query_file, missing where,
|
|
188
|
+
// unknown node types, placeholder typos, etc. before the engine sees them.
|
|
189
|
+
if (!skip_validation) {
|
|
190
|
+
try {
|
|
191
|
+
const valResult = await runValidation(rootDir, targetProjectId)
|
|
192
|
+
const relevantErrors = (valResult.diagnostics || []).filter(d =>
|
|
193
|
+
d.severity === "error" && d.endpoint === found.doc.name
|
|
194
|
+
)
|
|
195
|
+
if (relevantErrors.length > 0) {
|
|
196
|
+
return {
|
|
197
|
+
success: false,
|
|
198
|
+
phase: "pre_flight_validation",
|
|
199
|
+
endpoint,
|
|
200
|
+
file: found.full.replace(rootDir + "/", ""),
|
|
201
|
+
error: `${relevantErrors.length} validation error(s) in this endpoint — fix them before testing.`,
|
|
202
|
+
errors: relevantErrors,
|
|
203
|
+
hint: "Each error includes a fix_hint. Address them and re-run dypai_test_endpoint. Pass skip_validation: true if you want to bypass this and see the raw engine response.",
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch (e) {
|
|
207
|
+
// Non-fatal: if validation itself crashes, continue to the test so
|
|
208
|
+
// the agent at least gets the engine's response. Logged as a warning.
|
|
209
|
+
process.stderr.write(`[dypai_test_endpoint] pre-flight validation failed: ${e.message}\n`)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
175
213
|
// Deserialize the local YAML to the engine-shaped workflow_code
|
|
176
214
|
const deserCtx = { ...mapsCtx, readFile: (p) => fileMap[p] ?? "" }
|
|
177
215
|
let row
|
|
@@ -197,13 +235,41 @@ export const dypaiTestEndpointTool = {
|
|
|
197
235
|
|
|
198
236
|
try {
|
|
199
237
|
const result = await proxyToolCall("test_workflow", execArgs)
|
|
238
|
+
|
|
239
|
+
// Defensive: the remote sometimes returns a plain error string (not a
|
|
240
|
+
// structured object) when something fails outside the workflow trace —
|
|
241
|
+
// e.g. SQL operator errors before the trace is built. Spreading a string
|
|
242
|
+
// with `...` would explode it character-by-character into the response,
|
|
243
|
+
// which is what produced the {"0":"o","1":"p",...} bug. Detect and wrap.
|
|
244
|
+
if (typeof result === "string") {
|
|
245
|
+
return {
|
|
246
|
+
success: false,
|
|
247
|
+
endpoint,
|
|
248
|
+
file: found.full.replace(rootDir + "/", ""),
|
|
249
|
+
as_user: as_user || null,
|
|
250
|
+
error: result.length > 2000 ? result.slice(0, 2000) + "...[truncated]" : result,
|
|
251
|
+
hint: "The remote returned a raw error string (no per-node trace available). Read the error above for the root cause.",
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (!result || typeof result !== "object") {
|
|
255
|
+
return {
|
|
256
|
+
success: false,
|
|
257
|
+
endpoint,
|
|
258
|
+
file: found.full.replace(rootDir + "/", ""),
|
|
259
|
+
as_user: as_user || null,
|
|
260
|
+
error: `Unexpected response type from remote test_workflow: ${typeof result}`,
|
|
261
|
+
raw_response: result,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
200
265
|
// Summarize the trace just like the direct test_workflow path.
|
|
201
266
|
const summarized = summarizeTestWorkflowResponse(result, trace_mode)
|
|
267
|
+
const safeSummary = (summarized && typeof summarized === "object" && !Array.isArray(summarized)) ? summarized : { result: summarized }
|
|
202
268
|
return {
|
|
203
269
|
endpoint,
|
|
204
270
|
file: found.full.replace(rootDir + "/", ""),
|
|
205
271
|
as_user: as_user || null,
|
|
206
|
-
...
|
|
272
|
+
...safeSummary,
|
|
207
273
|
}
|
|
208
274
|
} catch (e) {
|
|
209
275
|
return {
|