@dypai-ai/mcp 1.6.20 → 1.7.1
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/generated/serverInstructions.js +3 -3
- package/src/index.js +82 -8
- package/src/tools/deploy.js +142 -4
- package/src/tools/project-deploy-production.js +9 -0
- package/src/tools/storage.js +437 -72
- package/src/tools/sync/generate-types.js +17 -1
- package/src/tools/sync/planner.js +40 -3
- package/src/tools/sync/push.js +22 -3
- package/src/tools/sync/test-endpoint.js +7 -6
- package/src/tools/sync/validate.js +74 -1
- package/src/tools/trace-summarize.js +109 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* trigger blocks, and other noise don't show up as false-positive diffs.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { readFile, readdir } from "fs/promises"
|
|
11
|
+
import { readFile, readdir, mkdir, writeFile } from "fs/promises"
|
|
12
12
|
import { join } from "path"
|
|
13
13
|
import YAML from "yaml"
|
|
14
14
|
import { proxyToolCall } from "../proxy.js"
|
|
@@ -71,6 +71,33 @@ export async function readLocalStateSnapshot(rootDir) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Refresh .dypai/state.json from live remote endpoint timestamps.
|
|
76
|
+
* Call after publish/deploy so the next push does not see phantom conflicts.
|
|
77
|
+
*/
|
|
78
|
+
export async function refreshLocalStateSnapshot({ projectId, rootDir }) {
|
|
79
|
+
if (!projectId || !rootDir) return null
|
|
80
|
+
const rows = await execSql(projectId, `
|
|
81
|
+
SELECT id, name, updated_at
|
|
82
|
+
FROM system.endpoints
|
|
83
|
+
ORDER BY name
|
|
84
|
+
`)
|
|
85
|
+
const successfullyPulledIds = new Set(rows.map((row) => row.id))
|
|
86
|
+
const state = {
|
|
87
|
+
pulled_at: new Date().toISOString(),
|
|
88
|
+
project_id: projectId,
|
|
89
|
+
endpoints: Object.fromEntries(
|
|
90
|
+
rows
|
|
91
|
+
.filter((row) => successfullyPulledIds.has(row.id))
|
|
92
|
+
.map((row) => [row.name, { id: row.id, updated_at: row.updated_at }]),
|
|
93
|
+
),
|
|
94
|
+
}
|
|
95
|
+
const statePath = join(rootDir, ".dypai", "state.json")
|
|
96
|
+
await mkdir(join(rootDir, ".dypai"), { recursive: true })
|
|
97
|
+
await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8")
|
|
98
|
+
return state
|
|
99
|
+
}
|
|
100
|
+
|
|
74
101
|
/**
|
|
75
102
|
* Load dypai.config.yaml if it exists. This is the committed identity of the
|
|
76
103
|
* project (project_id, project_name). Used to auto-resolve project_id and to
|
|
@@ -138,7 +165,8 @@ export async function fetchRemoteState(projectId) {
|
|
|
138
165
|
// same session concurrently, especially right after a local restart.
|
|
139
166
|
const endpoints = await execSql(projectId, `
|
|
140
167
|
SELECT id, name, method, description, workflow_code, input, output,
|
|
141
|
-
allowed_roles, is_tool, tool_description, group_id, is_active,
|
|
168
|
+
allowed_roles, is_tool, tool_description, group_id, is_active,
|
|
169
|
+
response_cardinality, updated_at
|
|
142
170
|
FROM system.endpoints
|
|
143
171
|
ORDER BY name
|
|
144
172
|
`)
|
|
@@ -571,11 +599,20 @@ function inlineFileRefs(node, fileMap) {
|
|
|
571
599
|
* are brought to this shape so file contents are part of the comparison
|
|
572
600
|
* (otherwise identical file paths would mask content changes).
|
|
573
601
|
*/
|
|
602
|
+
function normalizeComparableDoc(doc) {
|
|
603
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return doc
|
|
604
|
+
const copy = { ...doc }
|
|
605
|
+
if (copy.response_cardinality == null || copy.response_cardinality === "") {
|
|
606
|
+
delete copy.response_cardinality
|
|
607
|
+
}
|
|
608
|
+
return copy
|
|
609
|
+
}
|
|
610
|
+
|
|
574
611
|
function remoteToComparable(row, mapsCtx) {
|
|
575
612
|
const { doc, extractedFiles } = serializeEndpoint(row, mapsCtx)
|
|
576
613
|
const fileMap = Object.fromEntries(extractedFiles.map(f => [f.path, f.content]))
|
|
577
614
|
inlineFileRefs(doc, fileMap)
|
|
578
|
-
return doc
|
|
615
|
+
return normalizeComparableDoc(doc)
|
|
579
616
|
}
|
|
580
617
|
|
|
581
618
|
function flowPayloadToRow(payload) {
|
package/src/tools/sync/push.js
CHANGED
|
@@ -264,6 +264,20 @@ function endpointPayload(row) {
|
|
|
264
264
|
return p
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
function splitBlockingConflicts(plan, conflicts) {
|
|
268
|
+
const touched = new Set([
|
|
269
|
+
...(plan.update || []).map((item) => item.name),
|
|
270
|
+
...(plan.delete || []).map((item) => item.name),
|
|
271
|
+
])
|
|
272
|
+
const blocking = []
|
|
273
|
+
const non_blocking = []
|
|
274
|
+
for (const conflict of conflicts || []) {
|
|
275
|
+
if (touched.has(conflict.endpoint)) blocking.push(conflict)
|
|
276
|
+
else non_blocking.push(conflict)
|
|
277
|
+
}
|
|
278
|
+
return { blocking, non_blocking }
|
|
279
|
+
}
|
|
280
|
+
|
|
267
281
|
/**
|
|
268
282
|
* Treat a remote tool response as a definite success only when it has at
|
|
269
283
|
* least one of the markers we expect from a real mutation. Anything else
|
|
@@ -631,13 +645,15 @@ export const dypaiPushTool = {
|
|
|
631
645
|
|
|
632
646
|
// Block push on conflicts unless forced
|
|
633
647
|
const conflicts = (plan.warnings || []).filter(w => w.type === "remote_changed_since_pull")
|
|
634
|
-
|
|
648
|
+
const conflictSplit = splitBlockingConflicts(plan, conflicts)
|
|
649
|
+
if (conflictSplit.blocking.length && !force) {
|
|
635
650
|
return {
|
|
636
651
|
success: false,
|
|
637
652
|
applied: false,
|
|
638
653
|
reason: "conflicts_detected",
|
|
639
|
-
conflicts,
|
|
640
|
-
|
|
654
|
+
conflicts: conflictSplit.blocking,
|
|
655
|
+
non_blocking_conflicts: conflictSplit.non_blocking.length ? conflictSplit.non_blocking : undefined,
|
|
656
|
+
hint: "Remote changes overlap endpoints this push would update/delete. Run dypai_pull to refresh local state, resolve the overlap, then push again. To override, pass force=true.",
|
|
641
657
|
}
|
|
642
658
|
}
|
|
643
659
|
|
|
@@ -663,6 +679,7 @@ export const dypaiPushTool = {
|
|
|
663
679
|
: totalChanges === 0 ? "no_changes" : "dry_run",
|
|
664
680
|
summary: summaryFromPlan(plan),
|
|
665
681
|
plan,
|
|
682
|
+
non_blocking_conflicts: conflictSplit.non_blocking.length ? conflictSplit.non_blocking : undefined,
|
|
666
683
|
types: typesGenerated,
|
|
667
684
|
automation_sync: automationSync,
|
|
668
685
|
source_checkpoint: sourceCheckpoint || undefined,
|
|
@@ -861,6 +878,7 @@ export const dypaiPushTool = {
|
|
|
861
878
|
},
|
|
862
879
|
details: applied,
|
|
863
880
|
endpoint_results,
|
|
881
|
+
non_blocking_conflicts: conflictSplit.non_blocking.length ? conflictSplit.non_blocking : undefined,
|
|
864
882
|
errors: errors.length ? errors : undefined,
|
|
865
883
|
types: typesGenerated,
|
|
866
884
|
automation_sync: automationSync,
|
|
@@ -907,6 +925,7 @@ function summaryFromPlan(plan) {
|
|
|
907
925
|
export const __testing = {
|
|
908
926
|
normalizeResponseCardinality,
|
|
909
927
|
endpointPayload,
|
|
928
|
+
splitBlockingConflicts,
|
|
910
929
|
collectBackendSourceCheckpointFiles,
|
|
911
930
|
deletedBackendSourcePathsFromPlan,
|
|
912
931
|
syncAutomationMetadata,
|
|
@@ -424,8 +424,8 @@ export const dypaiTestEndpointTool = {
|
|
|
424
424
|
},
|
|
425
425
|
trace_mode: {
|
|
426
426
|
type: "string",
|
|
427
|
-
enum: ["smart", "full", "minimal"],
|
|
428
|
-
description: "How to summarize the returned trace. 'smart' (default) surfaces the failing node in detail; with stop_at_step, includes outputs for
|
|
427
|
+
enum: ["smart", "output_only", "full", "minimal"],
|
|
428
|
+
description: "How to summarize the returned trace. 'smart' (default) surfaces the failing node in detail; 'output_only' returns final endpoint output + compact timings without prompts/inputs; with stop_at_step, includes outputs for completed steps. 'full' returns everything; 'minimal' returns only status + duration.",
|
|
429
429
|
default: "smart",
|
|
430
430
|
},
|
|
431
431
|
root_dir: {
|
|
@@ -611,17 +611,18 @@ export const dypaiTestEndpointTool = {
|
|
|
611
611
|
: undefined
|
|
612
612
|
const summarized = summarizeTestWorkflowResponse(result, trace_mode, traceOptions)
|
|
613
613
|
const safeSummary = (summarized && typeof summarized === "object" && !Array.isArray(summarized)) ? summarized : { result: summarized }
|
|
614
|
+
const compactTrace = trace_mode === "minimal"
|
|
614
615
|
|
|
615
616
|
return {
|
|
616
617
|
operation: "run",
|
|
617
618
|
endpoint,
|
|
618
619
|
source: sourceMetaRun,
|
|
619
620
|
as_user: as_user || null,
|
|
620
|
-
sdk_format_note: SDK_RESPONSE_FORMAT_NOTE,
|
|
621
|
-
sdk_response: sdkResponseFromEndpointTest(result),
|
|
622
|
-
frontend_usage: frontendUsageForEndpoint({ endpoint, method: sourceMetaRun.method, input }),
|
|
621
|
+
...(compactTrace ? {} : { sdk_format_note: SDK_RESPONSE_FORMAT_NOTE }),
|
|
622
|
+
...(compactTrace ? {} : { sdk_response: sdkResponseFromEndpointTest(result) }),
|
|
623
|
+
...(compactTrace ? {} : { frontend_usage: frontendUsageForEndpoint({ endpoint, method: sourceMetaRun.method, input }) }),
|
|
623
624
|
...(stopAtStep ? { stop_at_step: stopAtStep } : {}),
|
|
624
|
-
...(step_outputs
|
|
625
|
+
...(compactTrace || !step_outputs || !Object.keys(step_outputs).length ? {} : { step_outputs }),
|
|
625
626
|
...safeSummary,
|
|
626
627
|
...(stopAtStep && !step_outputs ? {
|
|
627
628
|
hint: "No per-step outputs in trace. Try trace_mode:'full' or inspect execution_id with search_logs(include_trace:true).",
|
|
@@ -932,6 +932,15 @@ function buildNodeOutputContracts(nodes, fileMap, ctx) {
|
|
|
932
932
|
continue
|
|
933
933
|
}
|
|
934
934
|
|
|
935
|
+
if (op === "semantic_search") {
|
|
936
|
+
contracts.set(node.id, {
|
|
937
|
+
type: "object",
|
|
938
|
+
properties: new Set(["ok", "operation", "table", "query", "results_count", "matches", "semantic_index"]),
|
|
939
|
+
strict: true,
|
|
940
|
+
})
|
|
941
|
+
continue
|
|
942
|
+
}
|
|
943
|
+
|
|
935
944
|
if (op === "insert") {
|
|
936
945
|
const bulk = nodeField(node, params, "mode") === "bulk" || Array.isArray(nodeField(node, params, "data"))
|
|
937
946
|
contracts.set(node.id, {
|
|
@@ -1429,6 +1438,9 @@ function validateEndpoint(entry, ctx) {
|
|
|
1429
1438
|
if (op === "mutation" && typeof table === "string") {
|
|
1430
1439
|
referencedTables.add(table)
|
|
1431
1440
|
}
|
|
1441
|
+
if (op === "semantic_search" && typeof table === "string") {
|
|
1442
|
+
referencedTables.add(table)
|
|
1443
|
+
}
|
|
1432
1444
|
// Legacy ops like `select` / `insert` / `update` / `delete` use `table:`
|
|
1433
1445
|
// as the target table directly.
|
|
1434
1446
|
if (op && LEGACY_OPS_THAT_USE_TABLE_FIELD.has(op) && typeof table === "string") {
|
|
@@ -1549,6 +1561,41 @@ function validateEndpoint(entry, ctx) {
|
|
|
1549
1561
|
})
|
|
1550
1562
|
}
|
|
1551
1563
|
}
|
|
1564
|
+
|
|
1565
|
+
// ── operation: semantic_search coherence ─────────────────────────────
|
|
1566
|
+
// Runtime semantic search is read-only over tables enabled through
|
|
1567
|
+
// manage_table_semantics. It uses `search_query` rather than `query`
|
|
1568
|
+
// because SQL query fields are rendered raw by the engine.
|
|
1569
|
+
if (op === "semantic_search") {
|
|
1570
|
+
const searchQuery = nodeField(node, params, "search_query")
|
|
1571
|
+
if (!table) {
|
|
1572
|
+
diagnostics.push({
|
|
1573
|
+
severity: "error",
|
|
1574
|
+
rule: "semantic_search_missing_table",
|
|
1575
|
+
endpoint: name, file, loc: `workflow.nodes[${node.id}]`,
|
|
1576
|
+
message: `Node '${node.id}' uses 'operation: semantic_search' but is missing 'table_name:'.`,
|
|
1577
|
+
fix_hint: `Add 'table_name: <public_table>' and enable the table with manage_table_semantics(operation:"enable").`,
|
|
1578
|
+
})
|
|
1579
|
+
}
|
|
1580
|
+
if (!searchQuery && !query) {
|
|
1581
|
+
diagnostics.push({
|
|
1582
|
+
severity: "error",
|
|
1583
|
+
rule: "semantic_search_missing_query",
|
|
1584
|
+
endpoint: name, file, loc: `workflow.nodes[${node.id}]`,
|
|
1585
|
+
message: `Node '${node.id}' uses 'operation: semantic_search' but is missing 'search_query:'.`,
|
|
1586
|
+
fix_hint: `Add 'search_query: \${input.query}'. Do not use hand-written pgvector SQL.`,
|
|
1587
|
+
})
|
|
1588
|
+
}
|
|
1589
|
+
if (!searchQuery && query) {
|
|
1590
|
+
diagnostics.push({
|
|
1591
|
+
severity: "warn",
|
|
1592
|
+
rule: "semantic_search_legacy_query_field",
|
|
1593
|
+
endpoint: name, file, loc: `workflow.nodes[${node.id}].query`,
|
|
1594
|
+
message: `Node '${node.id}' uses 'query:' for semantic_search. Prefer 'search_query:' so placeholders are template-rendered.`,
|
|
1595
|
+
fix_hint: `Rename 'query:' to 'search_query:'.`,
|
|
1596
|
+
})
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1552
1599
|
}
|
|
1553
1600
|
if (node.tool_ids !== undefined && node.tools === undefined) {
|
|
1554
1601
|
const toolIds = Array.isArray(node.tool_ids) ? node.tool_ids : []
|
|
@@ -1898,7 +1945,13 @@ function validateEndpoint(entry, ctx) {
|
|
|
1898
1945
|
|
|
1899
1946
|
// Agent structured output + tools — engine uses generateObject OR generateText+tools, not both.
|
|
1900
1947
|
if (nodeType === "agent") {
|
|
1901
|
-
const tools = Array.isArray(node.tools)
|
|
1948
|
+
const tools = Array.isArray(node.tools)
|
|
1949
|
+
? node.tools
|
|
1950
|
+
: Array.isArray(node.parameters?.tool_ids)
|
|
1951
|
+
? node.parameters.tool_ids
|
|
1952
|
+
: Array.isArray(node.tool_ids)
|
|
1953
|
+
? node.tool_ids
|
|
1954
|
+
: []
|
|
1902
1955
|
const hasTools = tools.length > 0
|
|
1903
1956
|
const hasOutputSchema =
|
|
1904
1957
|
node.output_schema != null &&
|
|
@@ -1917,6 +1970,26 @@ function validateEndpoint(entry, ctx) {
|
|
|
1917
1970
|
fix_hint: "search_docs('document extraction ocr')",
|
|
1918
1971
|
})
|
|
1919
1972
|
}
|
|
1973
|
+
|
|
1974
|
+
const activeWorkspace = node.active_workspace_id ?? node.parameters?.active_workspace_id
|
|
1975
|
+
const usesWorkspaceTools = tools.some((toolName) => String(toolName).startsWith("workspace-"))
|
|
1976
|
+
const hasActiveWorkspace =
|
|
1977
|
+
typeof activeWorkspace === "string"
|
|
1978
|
+
? activeWorkspace.trim().length > 0
|
|
1979
|
+
: activeWorkspace != null && typeof activeWorkspace === "object"
|
|
1980
|
+
if (usesWorkspaceTools && !hasActiveWorkspace) {
|
|
1981
|
+
diagnostics.push({
|
|
1982
|
+
severity: "error",
|
|
1983
|
+
rule: "agent_workspace_binding_missing",
|
|
1984
|
+
endpoint: name,
|
|
1985
|
+
file,
|
|
1986
|
+
loc: `workflow.nodes[${node.id}]`,
|
|
1987
|
+
message:
|
|
1988
|
+
"agent uses workspace-* tools but active_workspace_id / activeWorkspace is not configured.",
|
|
1989
|
+
fix_hint:
|
|
1990
|
+
"Create a workspace step, then bind it with .agent({ activeWorkspace: ref.step('workspace', 'workspace_id'), tools: [...] }).",
|
|
1991
|
+
})
|
|
1992
|
+
}
|
|
1920
1993
|
}
|
|
1921
1994
|
}
|
|
1922
1995
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* - "smart" (default) — success → minimal; failure → focused on failing node
|
|
12
12
|
* - "full" — raw trace (opt-in, for deep dives)
|
|
13
13
|
* - "minimal" — just success/failure + duration + execution_order
|
|
14
|
+
* - "output_only" — final endpoint output + compact node timings, no inputs/snapshots
|
|
14
15
|
*
|
|
15
16
|
* The raw `events[]` array (node-scoped event log) is always dropped from
|
|
16
17
|
* "smart" and "minimal" — it's redundant with the parsed fields.
|
|
@@ -18,11 +19,14 @@
|
|
|
18
19
|
|
|
19
20
|
const MAX_FIELD_CHARS = 2000 // per-string truncation threshold
|
|
20
21
|
const MAX_SNAPSHOT_KEYS = 6 // summarize large snapshots into a key outline
|
|
22
|
+
const MAX_OUTPUT_CHARS = 8000
|
|
23
|
+
const MAX_OUTPUT_KEYS = 40
|
|
24
|
+
const MAX_OUTPUT_ITEMS = 30
|
|
21
25
|
|
|
22
26
|
function truncateString(s, max = MAX_FIELD_CHARS) {
|
|
23
27
|
if (typeof s !== "string") return s
|
|
24
28
|
if (s.length <= max) return s
|
|
25
|
-
return s.slice(0, max) + `... [truncated ${s.length - max} chars,
|
|
29
|
+
return s.slice(0, max) + `... [truncated ${s.length - max} chars, use trace_mode:'full' or search_logs(include_trace:true)]`
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
function summarizeValue(v) {
|
|
@@ -49,6 +53,41 @@ function summarizeValue(v) {
|
|
|
49
53
|
return v
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
function summarizeOutputValue(v) {
|
|
57
|
+
if (v == null) return v
|
|
58
|
+
if (typeof v === "string") return truncateString(v, MAX_OUTPUT_CHARS)
|
|
59
|
+
if (Array.isArray(v)) {
|
|
60
|
+
const items = v.slice(0, MAX_OUTPUT_ITEMS).map(summarizeOutputValue)
|
|
61
|
+
if (v.length > MAX_OUTPUT_ITEMS) {
|
|
62
|
+
items.push(`... ${v.length - MAX_OUTPUT_ITEMS} more items`)
|
|
63
|
+
}
|
|
64
|
+
return items
|
|
65
|
+
}
|
|
66
|
+
if (typeof v === "object") {
|
|
67
|
+
const keys = Object.keys(v)
|
|
68
|
+
const out = {}
|
|
69
|
+
for (const k of keys.slice(0, MAX_OUTPUT_KEYS)) {
|
|
70
|
+
out[k] = summarizeOutputValue(v[k])
|
|
71
|
+
}
|
|
72
|
+
if (keys.length > MAX_OUTPUT_KEYS) out._truncated_keys = keys.length - MAX_OUTPUT_KEYS
|
|
73
|
+
return out
|
|
74
|
+
}
|
|
75
|
+
return v
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hasOwn(obj, key) {
|
|
79
|
+
return Object.prototype.hasOwnProperty.call(obj, key)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function extractEndpointOutput(response) {
|
|
83
|
+
if (!response || typeof response !== "object" || Array.isArray(response)) return undefined
|
|
84
|
+
if (hasOwn(response, "result")) return response.result
|
|
85
|
+
if (hasOwn(response, "output")) return response.output
|
|
86
|
+
if (hasOwn(response, "data")) return response.data
|
|
87
|
+
if (hasOwn(response, "response")) return response.response
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
52
91
|
/** Build a hint string from the error message based on common patterns. */
|
|
53
92
|
function hintFromError(err) {
|
|
54
93
|
const m = String(err?.message || "")
|
|
@@ -81,6 +120,45 @@ export function summarizeTrace(trace, mode = "smart", options = {}) {
|
|
|
81
120
|
}
|
|
82
121
|
}
|
|
83
122
|
|
|
123
|
+
if (mode === "output_only") {
|
|
124
|
+
let failedAt = null
|
|
125
|
+
const nodeSummaries = []
|
|
126
|
+
|
|
127
|
+
for (const id of execOrder) {
|
|
128
|
+
const n = nodes[id]
|
|
129
|
+
if (!n) continue
|
|
130
|
+
const node = {
|
|
131
|
+
id: n.node_id,
|
|
132
|
+
type: n.node_type,
|
|
133
|
+
status: n.status,
|
|
134
|
+
duration_ms: n.duration_ms,
|
|
135
|
+
}
|
|
136
|
+
if (n.status === "failed") {
|
|
137
|
+
failedAt = failedAt || n.node_id
|
|
138
|
+
node.error = {
|
|
139
|
+
message: truncateString(n.error?.message),
|
|
140
|
+
type: n.error?.type,
|
|
141
|
+
}
|
|
142
|
+
if (n.fix_hint || n.error?.fix_hint) {
|
|
143
|
+
node.fix_hint = truncateString(n.fix_hint || n.error?.fix_hint)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
nodeSummaries.push(node)
|
|
147
|
+
if (stopAtStep && id === stopAtStep) break
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
execution_id: trace.execution_id,
|
|
152
|
+
success: wf.status === "completed",
|
|
153
|
+
status: wf.status,
|
|
154
|
+
duration_ms: wf.duration_ms,
|
|
155
|
+
failed_at: failedAt || undefined,
|
|
156
|
+
execution_order: execOrder,
|
|
157
|
+
nodes: nodeSummaries,
|
|
158
|
+
hint: "Compact trace only. Use trace_mode:'smart' for failing-node context or trace_mode:'full' for raw inputs/snapshots.",
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
84
162
|
// "smart" mode
|
|
85
163
|
const failed = wf.status === "failed"
|
|
86
164
|
const failedNodeId = failed
|
|
@@ -170,6 +248,36 @@ export function summarizeTrace(trace, mode = "smart", options = {}) {
|
|
|
170
248
|
*/
|
|
171
249
|
export function summarizeTestWorkflowResponse(response, mode = "smart", options = {}) {
|
|
172
250
|
if (!response || typeof response !== "object") return response
|
|
251
|
+
if (mode === "minimal") {
|
|
252
|
+
const trace = response.trace ? summarizeTrace(response.trace, mode, options) : undefined
|
|
253
|
+
const output = extractEndpointOutput(response)
|
|
254
|
+
const out = {
|
|
255
|
+
success: trace?.status ? trace.status === "completed" : (response.error ? false : undefined),
|
|
256
|
+
execution_id: response.execution_id || trace?.execution_id,
|
|
257
|
+
status: trace?.status || response.status,
|
|
258
|
+
duration_ms: trace?.duration_ms || response.duration_ms,
|
|
259
|
+
...(output !== undefined ? { result: summarizeOutputValue(output) } : {}),
|
|
260
|
+
...(response.error ? { error: summarizeOutputValue(response.error) } : {}),
|
|
261
|
+
...(trace ? { trace } : {}),
|
|
262
|
+
hint: "Returned endpoint result only. Use trace_mode:'smart' for failure context or trace_mode:'full' for raw execution snapshots.",
|
|
263
|
+
}
|
|
264
|
+
return Object.fromEntries(Object.entries(out).filter(([, v]) => v !== undefined))
|
|
265
|
+
}
|
|
266
|
+
if (mode === "output_only") {
|
|
267
|
+
const trace = response.trace ? summarizeTrace(response.trace, mode, options) : undefined
|
|
268
|
+
const output = extractEndpointOutput(response)
|
|
269
|
+
const out = {
|
|
270
|
+
success: trace?.success ?? (response.error ? false : undefined),
|
|
271
|
+
execution_id: response.execution_id || trace?.execution_id,
|
|
272
|
+
status: trace?.status || response.status,
|
|
273
|
+
duration_ms: trace?.duration_ms || response.duration_ms,
|
|
274
|
+
...(output !== undefined ? { result: summarizeOutputValue(output) } : {}),
|
|
275
|
+
...(response.error ? { error: summarizeOutputValue(response.error) } : {}),
|
|
276
|
+
...(trace ? { trace } : {}),
|
|
277
|
+
hint: "Returned final endpoint output only. Use trace_mode:'smart' for failure context, stop_at_step for per-step outputs, or trace_mode:'full' for raw trace.",
|
|
278
|
+
}
|
|
279
|
+
return Object.fromEntries(Object.entries(out).filter(([, v]) => v !== undefined))
|
|
280
|
+
}
|
|
173
281
|
if (!response.trace) return response
|
|
174
282
|
return {
|
|
175
283
|
...response,
|