@plasm_lang/vercel-agent 0.3.78 → 0.3.81

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 (49) hide show
  1. package/agent/instructions.md +1 -2
  2. package/bin/plasm-agent.mjs +5 -7
  3. package/package.json +26 -19
  4. package/scripts/plasm-cli.ts +4 -3
  5. package/scripts/run-plasm-cli.mjs +41 -0
  6. package/src/cli/init-scaffold.ts +198 -0
  7. package/src/cli/init.ts +16 -278
  8. package/src/cli/nitro-dev.ts +2 -6
  9. package/src/cli/node-dev-imports.ts +14 -0
  10. package/src/package-version.ts +14 -0
  11. package/src/project-info.ts +2 -1
  12. package/templates/mcp-radar/.vercelignore +3 -0
  13. package/templates/mcp-radar/README.md +139 -0
  14. package/templates/mcp-radar/agent/agent.ts +38 -0
  15. package/templates/mcp-radar/agent/catalogs/hackernews/README.md +23 -0
  16. package/templates/mcp-radar/agent/catalogs/hackernews/domain.yaml +329 -0
  17. package/templates/mcp-radar/agent/catalogs/hackernews/eval/cases.yaml +112 -0
  18. package/templates/mcp-radar/agent/catalogs/hackernews/mappings.yaml +129 -0
  19. package/templates/mcp-radar/agent/catalogs/tavily/README.md +218 -0
  20. package/templates/mcp-radar/agent/catalogs/tavily/domain.yaml +373 -0
  21. package/templates/mcp-radar/agent/catalogs/tavily/eval/cases.yaml +56 -0
  22. package/templates/mcp-radar/agent/catalogs/tavily/eval/google-gemma-3-4b-it.latest.human.txt +67 -0
  23. package/templates/mcp-radar/agent/catalogs/tavily/eval/google-gemma-3-4b-it.latest.json +363 -0
  24. package/templates/mcp-radar/agent/catalogs/tavily/mappings.yaml +171 -0
  25. package/templates/mcp-radar/agent/channels/mcp-radar.ts +85 -0
  26. package/templates/mcp-radar/agent/hooks/proof-audit.ts +10 -0
  27. package/templates/mcp-radar/agent/instructions.md +24 -0
  28. package/templates/mcp-radar/agent/research/mcp-innovations-proof.md +4 -0
  29. package/templates/mcp-radar/agent/schedules/mcp-radar-scan.ts +14 -0
  30. package/templates/mcp-radar/agent/skills/mcp-proof-format.md +19 -0
  31. package/templates/mcp-radar/api/[[...path]].ts +23 -0
  32. package/templates/mcp-radar/evals/mcp-radar-discover.eval.ts +14 -0
  33. package/templates/mcp-radar/lib/proof-store.ts +265 -0
  34. package/templates/mcp-radar/lib/run-radar.ts +214 -0
  35. package/templates/mcp-radar/nitro.config.ts +17 -0
  36. package/templates/mcp-radar/package.json +14 -0
  37. package/templates/mcp-radar/public/index.html +10 -0
  38. package/templates/mcp-radar/routes/[...path].ts +29 -0
  39. package/templates/mcp-radar/scripts/run-evals.ts +46 -0
  40. package/templates/mcp-radar/scripts/smoke-channel.ts +66 -0
  41. package/templates/mcp-radar/vercel.json +15 -0
  42. package/templates/scaffold/.vercelignore +3 -0
  43. package/templates/scaffold/api/[[...path]].ts +23 -0
  44. package/templates/scaffold/nitro.config.ts +17 -0
  45. package/templates/scaffold/public/index.html +10 -0
  46. package/templates/scaffold/public/index.mcp-radar.html +10 -0
  47. package/templates/scaffold/routes/[...path].ts +29 -0
  48. package/templates/scaffold/vercel.default.json +4 -0
  49. package/templates/scaffold/vercel.mcp-radar.json +15 -0
@@ -0,0 +1,67 @@
1
+ model: google/gemma-3-4b-it
2
+ eval summary: 7 cases | mean score 0.7982 | parse_ok 6/7 | typecheck_ok 6/7
3
+
4
+ ════════════════════════════════════════════════════════════════════════
5
+ Pipeline failures (parse/typecheck never succeeded): 1
6
+
7
+ ▸ tv-07 final_score=0.0000 semantic=0.000
8
+ goal: Web search for 'rust async' but return only titles and URLs in the result list
9
+ rounds=2 recovered=false pipeline_score=0.00
10
+ ─── attempt 1 ───
11
+ step 0: ExtractResult{query="rust async", ~str}
12
+ reason: Search for 'rust async' using full-text search and retrieve relevant ExtractResult entities.
13
+ validation: FAILED
14
+ • [parse] step 0 `ExtractResult{query="rust async", ~str}`
15
+ Fix spelling so identifiers, `=`, `{{}}`, `.`, and parentheses match the SYNTAX block in the prompt.
16
+ span_offset: 34
17
+ ─── attempt 2 ───
18
+ step 0: ExtractResult{query="rust async", ~str}
19
+ reason: Search for 'rust async' using full-text search and retrieve relevant ExtractResult entities.
20
+ correction_context_in (feedback from prior round, sent to LLM):
21
+ {
22
+ "round": 0,
23
+ "goal": "Web search for 'rust async' but return only titles and URLs in the result list",
24
+ "previous_steps": [
25
+ {
26
+ "text": "ExtractResult{query=\"rust async\", ~str}",
27
+ "reasoning": "Search for 'rust async' using full-text search and retrieve relevant ExtractResult entities."
28
+ }
29
+ ],
30
+ "diagnostics": [
31
+ {
32
+ "step_index": 0,
33
+ "expression": "ExtractResult{query=\"rust async\", ~str}",
34
+ "error": {
35
+ "correction": "Fix spelling so identifiers, `=`, `{{}}`, `.`, and parentheses match the SYNTAX block in the prompt.",
36
+ "category": "parse",
37
+ "span_offset": 34
38
+ }
39
+ }
40
+ ]
41
+ }
42
+ validation: FAILED
43
+ • [parse] step 0 `ExtractResult{query="rust async", ~str}`
44
+ Fix spelling so identifiers, `=`, `{{}}`, `.`, and parentheses match the SYNTAX block in the prompt.
45
+ span_offset: 34
46
+
47
+
48
+ Expectation gaps (validated, soft score below 1.0): 2
49
+
50
+ ▸ tv-01 blended_score=0.7750 semantic=0.750
51
+ goal: Research briefing: what are the main risks of LLM prompt injection in 2025?
52
+ expectation notes:
53
+ - entities got {"ResearchTask"} want any of ["SearchResult"]
54
+ final plan (for context):
55
+ step 0: ResearchTask{query="main risks of LLM prompt injection in 2025"}
56
+ reason: Retrieve ResearchTasks related to the specified query to initiate the research.
57
+
58
+ ▸ tv-03 blended_score=0.8125 semantic=0.750
59
+ goal: Crawl the product marketing site up to depth 2 for a competitive summary
60
+ expectation notes:
61
+ - entities got {"ResearchTask"} want any of ["ExtractResult"]
62
+ final plan (for context):
63
+ step 0: ResearchTask{scope=Source(id), filter=marketing}
64
+ reason: Start by finding ResearchTasks specifically related to marketing sources.
65
+
66
+ ════════════════════════════════════════════════════════════════════════
67
+
@@ -0,0 +1,363 @@
1
+ {
2
+ "cases": [
3
+ {
4
+ "attempt_trace": [
5
+ {
6
+ "attempt": 1,
7
+ "diagnostics": [
8
+ {
9
+ "error": {
10
+ "category": "parse",
11
+ "correction": "Expression: ResearchTask(42).method\n\n`method` is not a valid name after `ResearchTask.` (field, relation, or zero-arg action). Closest names in DOMAIN: model, content, input, status, request_id.\n\nAllowed names (8): content, created_at, input, model, request_id, response_time, sources, status\n\nFor reading scalar fields: `ResearchTask(id)[status,content]`.\n\nTo navigate: `ResearchTask(id).content` (relation or EntityRef).",
12
+ "span_offset": 17
13
+ },
14
+ "expression": "ResearchTask(42).method",
15
+ "step_index": 0
16
+ }
17
+ ],
18
+ "steps": [
19
+ {
20
+ "reasoning": "Retrieve the ResearchTask with ID 42 to access its method.",
21
+ "text": "ResearchTask(42).method"
22
+ }
23
+ ],
24
+ "validation_ok": false
25
+ },
26
+ {
27
+ "attempt": 2,
28
+ "correction_context_in": "{\n \"round\": 0,\n \"goal\": \"Research briefing: what are the main risks of LLM prompt injection in 2025?\",\n \"previous_steps\": [\n {\n \"text\": \"ResearchTask(42).method\",\n \"reasoning\": \"Retrieve the ResearchTask with ID 42 to access its method.\"\n }\n ],\n \"diagnostics\": [\n {\n \"step_index\": 0,\n \"expression\": \"ResearchTask(42).method\",\n \"error\": {\n \"correction\": \"Expression: ResearchTask(42).method\\n\\n`method` is not a valid name after `ResearchTask.` (field, relation, or zero-arg action). Closest names in DOMAIN: model, content, input, status, request_id.\\n\\nAllowed names (8): content, created_at, input, model, request_id, response_time, sources, status\\n\\nFor reading scalar fields: `ResearchTask(id)[status,content]`.\\n\\nTo navigate: `ResearchTask(id).content` (relation or EntityRef).\",\n \"category\": \"parse\",\n \"span_offset\": 17\n }\n }\n ]\n}",
29
+ "lexicon_notes": [
30
+ {
31
+ "emitted": "ResearchTask{query=\"main risks of LLM prompt injection in 2025\"}",
32
+ "resolved_to": "ResearchTask",
33
+ "step_index": 0
34
+ }
35
+ ],
36
+ "steps": [
37
+ {
38
+ "reasoning": "Retrieve ResearchTasks related to the specified query to initiate the research.",
39
+ "text": "ResearchTask{query=\"main risks of LLM prompt injection in 2025\"}"
40
+ }
41
+ ],
42
+ "validation_ok": true
43
+ }
44
+ ],
45
+ "chain_match": true,
46
+ "correction": {
47
+ "final_full_ok": true,
48
+ "first_attempt_full_ok": false,
49
+ "first_attempt_had_errors": true,
50
+ "pipeline_score": 0.8500000238418579,
51
+ "recovered": true,
52
+ "rounds_used": 2
53
+ },
54
+ "correction_pipeline_score": 0.8500000238418579,
55
+ "entity_match": false,
56
+ "goal": "Research briefing: what are the main risks of LLM prompt injection in 2025?",
57
+ "id": "tv-01",
58
+ "notes": [
59
+ "entities got {\"ResearchTask\"} want any of [\"SearchResult\"]"
60
+ ],
61
+ "parse_ok": true,
62
+ "pred_fields_match": true,
63
+ "pred_values_match": true,
64
+ "projection_match": true,
65
+ "score": 0.7749999761581421,
66
+ "semantic_score": 0.75,
67
+ "steps_ok": true,
68
+ "tags": [
69
+ "search"
70
+ ],
71
+ "typecheck_ok": true
72
+ },
73
+ {
74
+ "attempt_trace": [
75
+ {
76
+ "attempt": 1,
77
+ "steps": [
78
+ {
79
+ "reasoning": "To find URLs from the documentation.",
80
+ "text": "ExtractResult{query=\"urls~str\"}"
81
+ }
82
+ ],
83
+ "validation_ok": true
84
+ }
85
+ ],
86
+ "chain_match": true,
87
+ "correction": {
88
+ "final_full_ok": true,
89
+ "first_attempt_full_ok": true,
90
+ "first_attempt_had_errors": false,
91
+ "pipeline_score": 1.0,
92
+ "recovered": false,
93
+ "rounds_used": 1
94
+ },
95
+ "correction_pipeline_score": 1.0,
96
+ "entity_match": true,
97
+ "goal": "Extract readable text from these documentation URLs for offline notes",
98
+ "id": "tv-02",
99
+ "notes": [],
100
+ "parse_ok": true,
101
+ "pred_fields_match": true,
102
+ "pred_values_match": true,
103
+ "projection_match": true,
104
+ "score": 1.0,
105
+ "semantic_score": 1.0,
106
+ "steps_ok": true,
107
+ "tags": [
108
+ "extract"
109
+ ],
110
+ "typecheck_ok": true
111
+ },
112
+ {
113
+ "attempt_trace": [
114
+ {
115
+ "attempt": 1,
116
+ "lexicon_notes": [
117
+ {
118
+ "emitted": "ResearchTask{scope=Source(id), filter=marketing}",
119
+ "resolved_to": "ResearchTask",
120
+ "step_index": 0
121
+ }
122
+ ],
123
+ "steps": [
124
+ {
125
+ "reasoning": "Start by finding ResearchTasks specifically related to marketing sources.",
126
+ "text": "ResearchTask{scope=Source(id), filter=marketing}"
127
+ }
128
+ ],
129
+ "validation_ok": true
130
+ }
131
+ ],
132
+ "chain_match": true,
133
+ "correction": {
134
+ "final_full_ok": true,
135
+ "first_attempt_full_ok": true,
136
+ "first_attempt_had_errors": false,
137
+ "pipeline_score": 1.0,
138
+ "recovered": false,
139
+ "rounds_used": 1
140
+ },
141
+ "correction_pipeline_score": 1.0,
142
+ "entity_match": false,
143
+ "goal": "Crawl the product marketing site up to depth 2 for a competitive summary",
144
+ "id": "tv-03",
145
+ "notes": [
146
+ "entities got {\"ResearchTask\"} want any of [\"ExtractResult\"]"
147
+ ],
148
+ "parse_ok": true,
149
+ "pred_fields_match": true,
150
+ "pred_values_match": true,
151
+ "projection_match": true,
152
+ "score": 0.8125,
153
+ "semantic_score": 0.75,
154
+ "steps_ok": true,
155
+ "tags": [
156
+ "crawl"
157
+ ],
158
+ "typecheck_ok": true
159
+ },
160
+ {
161
+ "attempt_trace": [
162
+ {
163
+ "attempt": 1,
164
+ "steps": [
165
+ {
166
+ "reasoning": "To initiate a research task and obtain its ID.",
167
+ "text": "ResearchTask(id)"
168
+ }
169
+ ],
170
+ "validation_ok": true
171
+ }
172
+ ],
173
+ "chain_match": true,
174
+ "correction": {
175
+ "final_full_ok": true,
176
+ "first_attempt_full_ok": true,
177
+ "first_attempt_had_errors": false,
178
+ "pipeline_score": 1.0,
179
+ "recovered": false,
180
+ "rounds_used": 1
181
+ },
182
+ "correction_pipeline_score": 1.0,
183
+ "entity_match": true,
184
+ "goal": "Start a deep research job on quantum error correction and save the request id",
185
+ "id": "tv-04",
186
+ "notes": [],
187
+ "parse_ok": true,
188
+ "pred_fields_match": true,
189
+ "pred_values_match": true,
190
+ "projection_match": true,
191
+ "score": 1.0,
192
+ "semantic_score": 1.0,
193
+ "steps_ok": true,
194
+ "tags": [
195
+ "create"
196
+ ],
197
+ "typecheck_ok": true
198
+ },
199
+ {
200
+ "attempt_trace": [
201
+ {
202
+ "attempt": 1,
203
+ "steps": [
204
+ {
205
+ "reasoning": "To find the research task associated with the request identifier.",
206
+ "text": "ResearchTask(req_abc123)"
207
+ }
208
+ ],
209
+ "validation_ok": true
210
+ }
211
+ ],
212
+ "chain_match": true,
213
+ "correction": {
214
+ "final_full_ok": true,
215
+ "first_attempt_full_ok": true,
216
+ "first_attempt_had_errors": false,
217
+ "pipeline_score": 1.0,
218
+ "recovered": false,
219
+ "rounds_used": 1
220
+ },
221
+ "correction_pipeline_score": 1.0,
222
+ "entity_match": true,
223
+ "goal": "Poll research task req_abc123 until the report body is ready",
224
+ "id": "tv-05",
225
+ "notes": [],
226
+ "parse_ok": true,
227
+ "pred_fields_match": true,
228
+ "pred_values_match": true,
229
+ "projection_match": true,
230
+ "score": 1.0,
231
+ "semantic_score": 1.0,
232
+ "steps_ok": true,
233
+ "tags": [
234
+ "get"
235
+ ],
236
+ "typecheck_ok": true
237
+ },
238
+ {
239
+ "attempt_trace": [
240
+ {
241
+ "attempt": 1,
242
+ "steps": [
243
+ {
244
+ "reasoning": "The goal is to kick off async research on a topic, and the goal specifies the ResearchTask ID as 42.",
245
+ "text": "ResearchTask(42)"
246
+ }
247
+ ],
248
+ "validation_ok": true
249
+ }
250
+ ],
251
+ "chain_match": true,
252
+ "correction": {
253
+ "final_full_ok": true,
254
+ "first_attempt_full_ok": true,
255
+ "first_attempt_had_errors": false,
256
+ "pipeline_score": 1.0,
257
+ "recovered": false,
258
+ "rounds_used": 1
259
+ },
260
+ "correction_pipeline_score": 1.0,
261
+ "entity_match": true,
262
+ "goal": "Kick off async research on a topic, then fetch status when the user returns",
263
+ "id": "tv-06",
264
+ "notes": [],
265
+ "parse_ok": true,
266
+ "pred_fields_match": true,
267
+ "pred_values_match": true,
268
+ "projection_match": true,
269
+ "score": 1.0,
270
+ "semantic_score": 1.0,
271
+ "steps_ok": true,
272
+ "tags": [
273
+ "multi_step"
274
+ ],
275
+ "typecheck_ok": true
276
+ },
277
+ {
278
+ "attempt_trace": [
279
+ {
280
+ "attempt": 1,
281
+ "diagnostics": [
282
+ {
283
+ "error": {
284
+ "category": "parse",
285
+ "correction": "Fix spelling so identifiers, `=`, `{{}}`, `.`, and parentheses match the SYNTAX block in the prompt.",
286
+ "span_offset": 34
287
+ },
288
+ "expression": "ExtractResult{query=\"rust async\", ~str}",
289
+ "step_index": 0
290
+ }
291
+ ],
292
+ "steps": [
293
+ {
294
+ "reasoning": "Search for 'rust async' using full-text search and retrieve relevant ExtractResult entities.",
295
+ "text": "ExtractResult{query=\"rust async\", ~str}"
296
+ }
297
+ ],
298
+ "validation_ok": false
299
+ },
300
+ {
301
+ "attempt": 2,
302
+ "correction_context_in": "{\n \"round\": 0,\n \"goal\": \"Web search for 'rust async' but return only titles and URLs in the result list\",\n \"previous_steps\": [\n {\n \"text\": \"ExtractResult{query=\\\"rust async\\\", ~str}\",\n \"reasoning\": \"Search for 'rust async' using full-text search and retrieve relevant ExtractResult entities.\"\n }\n ],\n \"diagnostics\": [\n {\n \"step_index\": 0,\n \"expression\": \"ExtractResult{query=\\\"rust async\\\", ~str}\",\n \"error\": {\n \"correction\": \"Fix spelling so identifiers, `=`, `{{}}`, `.`, and parentheses match the SYNTAX block in the prompt.\",\n \"category\": \"parse\",\n \"span_offset\": 34\n }\n }\n ]\n}",
303
+ "diagnostics": [
304
+ {
305
+ "error": {
306
+ "category": "parse",
307
+ "correction": "Fix spelling so identifiers, `=`, `{{}}`, `.`, and parentheses match the SYNTAX block in the prompt.",
308
+ "span_offset": 34
309
+ },
310
+ "expression": "ExtractResult{query=\"rust async\", ~str}",
311
+ "step_index": 0
312
+ }
313
+ ],
314
+ "steps": [
315
+ {
316
+ "reasoning": "Search for 'rust async' using full-text search and retrieve relevant ExtractResult entities.",
317
+ "text": "ExtractResult{query=\"rust async\", ~str}"
318
+ }
319
+ ],
320
+ "validation_ok": false
321
+ }
322
+ ],
323
+ "chain_match": false,
324
+ "correction": {
325
+ "final_full_ok": false,
326
+ "first_attempt_full_ok": false,
327
+ "first_attempt_had_errors": true,
328
+ "last_failure": [
329
+ {
330
+ "error": {
331
+ "category": "parse",
332
+ "correction": "Fix spelling so identifiers, `=`, `{{}}`, `.`, and parentheses match the SYNTAX block in the prompt.",
333
+ "span_offset": 34
334
+ },
335
+ "expression": "ExtractResult{query=\"rust async\", ~str}",
336
+ "step_index": 0
337
+ }
338
+ ],
339
+ "pipeline_score": 0.0,
340
+ "recovered": false,
341
+ "rounds_used": 2
342
+ },
343
+ "correction_pipeline_score": 0.0,
344
+ "entity_match": false,
345
+ "goal": "Web search for 'rust async' but return only titles and URLs in the result list",
346
+ "id": "tv-07",
347
+ "notes": [],
348
+ "parse_ok": false,
349
+ "pred_fields_match": false,
350
+ "pred_values_match": false,
351
+ "projection_match": false,
352
+ "score": 0.0,
353
+ "semantic_score": 0.0,
354
+ "steps_ok": false,
355
+ "tags": [
356
+ "search",
357
+ "projection"
358
+ ],
359
+ "typecheck_ok": false
360
+ }
361
+ ],
362
+ "model": "google/gemma-3-4b-it"
363
+ }
@@ -0,0 +1,171 @@
1
+ # Tavily REST API — CML mappings. Domain model in domain.yaml.
2
+ #
3
+ # Base URL: Tavily HTTP API (set provider origin with --backend).
4
+ # All endpoints use POST except research_get (GET).
5
+ #
6
+ # All search/extract/crawl/research parameters go in the JSON request body,
7
+ # not the URL query string. The CML `body:` block is used throughout.
8
+ #
9
+ # Null-filtering: CML if/exists expressions emit null for absent optional
10
+ # params; the runtime strips null fields from the body before sending.
11
+ #
12
+ # include_domains / exclude_domains are sent as JSON arrays in the body:
13
+ # "include_domains": ["reuters.com", "bbc.com"]
14
+ # The `type: var` expression evaluates Value::Array which is serialised as
15
+ # a JSON array in the body (unlike query params where Array → repeated key).
16
+ #
17
+ # research_create returns a single object (not a list), decoded as `single`.
18
+ # research_get polls status — GET /research/{request_id}.
19
+
20
+ # ── Search ────────────────────────────────────────────────────────────────────
21
+
22
+ web_search:
23
+ method: POST
24
+ path:
25
+ - { type: literal, value: search }
26
+ response:
27
+ items: results
28
+ body:
29
+ type: object
30
+ fields:
31
+ - [query, { type: var, name: query }]
32
+ - [search_depth, { type: if, condition: { type: exists, var: search_depth },
33
+ then_expr: { type: var, name: search_depth },
34
+ else_expr: { type: const, value: null } }]
35
+ - [chunks_per_source, { type: if, condition: { type: exists, var: chunks_per_source },
36
+ then_expr: { type: var, name: chunks_per_source },
37
+ else_expr: { type: const, value: null } }]
38
+ - [max_results, { type: if, condition: { type: exists, var: max_results },
39
+ then_expr: { type: var, name: max_results },
40
+ else_expr: { type: const, value: null } }]
41
+ - [topic, { type: if, condition: { type: exists, var: topic },
42
+ then_expr: { type: var, name: topic },
43
+ else_expr: { type: const, value: null } }]
44
+ - [time_range, { type: if, condition: { type: exists, var: time_range },
45
+ then_expr: { type: var, name: time_range },
46
+ else_expr: { type: const, value: null } }]
47
+ - [start_date, { type: if, condition: { type: exists, var: start_date },
48
+ then_expr: { type: var, name: start_date },
49
+ else_expr: { type: const, value: null } }]
50
+ - [end_date, { type: if, condition: { type: exists, var: end_date },
51
+ then_expr: { type: var, name: end_date },
52
+ else_expr: { type: const, value: null } }]
53
+ - [include_answer, { type: if, condition: { type: exists, var: include_answer },
54
+ then_expr: { type: var, name: include_answer },
55
+ else_expr: { type: const, value: null } }]
56
+ - [include_raw_content, { type: if, condition: { type: exists, var: include_raw_content },
57
+ then_expr: { type: var, name: include_raw_content },
58
+ else_expr: { type: const, value: null } }]
59
+ - [include_images, { type: if, condition: { type: exists, var: include_images },
60
+ then_expr: { type: var, name: include_images },
61
+ else_expr: { type: const, value: null } }]
62
+ - [include_favicon, { type: if, condition: { type: exists, var: include_favicon },
63
+ then_expr: { type: var, name: include_favicon },
64
+ else_expr: { type: const, value: null } }]
65
+ - [include_domains, { type: if, condition: { type: exists, var: include_domains },
66
+ then_expr: { type: var, name: include_domains },
67
+ else_expr: { type: const, value: null } }]
68
+ - [exclude_domains, { type: if, condition: { type: exists, var: exclude_domains },
69
+ then_expr: { type: var, name: exclude_domains },
70
+ else_expr: { type: const, value: null } }]
71
+ - [country, { type: if, condition: { type: exists, var: country },
72
+ then_expr: { type: var, name: country },
73
+ else_expr: { type: const, value: null } }]
74
+ - [auto_parameters, { type: if, condition: { type: exists, var: auto_parameters },
75
+ then_expr: { type: var, name: auto_parameters },
76
+ else_expr: { type: const, value: null } }]
77
+ - [exact_match, { type: if, condition: { type: exists, var: exact_match },
78
+ then_expr: { type: var, name: exact_match },
79
+ else_expr: { type: const, value: null } }]
80
+
81
+ # ── Extract ───────────────────────────────────────────────────────────────────
82
+
83
+ url_extract:
84
+ method: POST
85
+ path:
86
+ - { type: literal, value: extract }
87
+ response:
88
+ items: results
89
+ body:
90
+ type: object
91
+ fields:
92
+ - [urls, { type: var, name: urls }]
93
+ - [query, { type: if, condition: { type: exists, var: query },
94
+ then_expr: { type: var, name: query },
95
+ else_expr: { type: const, value: null } }]
96
+ - [extract_depth, { type: if, condition: { type: exists, var: extract_depth },
97
+ then_expr: { type: var, name: extract_depth },
98
+ else_expr: { type: const, value: null } }]
99
+ - [format, { type: if, condition: { type: exists, var: format },
100
+ then_expr: { type: var, name: format },
101
+ else_expr: { type: const, value: null } }]
102
+ - [chunks_per_source, { type: if, condition: { type: exists, var: chunks_per_source },
103
+ then_expr: { type: var, name: chunks_per_source },
104
+ else_expr: { type: const, value: null } }]
105
+ - [include_images, { type: if, condition: { type: exists, var: include_images },
106
+ then_expr: { type: var, name: include_images },
107
+ else_expr: { type: const, value: null } }]
108
+ - [include_favicon, { type: if, condition: { type: exists, var: include_favicon },
109
+ then_expr: { type: var, name: include_favicon },
110
+ else_expr: { type: const, value: null } }]
111
+
112
+ # ── Crawl ─────────────────────────────────────────────────────────────────────
113
+
114
+ site_crawl:
115
+ method: POST
116
+ path:
117
+ - { type: literal, value: crawl }
118
+ response:
119
+ items: results
120
+ body:
121
+ type: object
122
+ fields:
123
+ - [url, { type: var, name: url }]
124
+ - [instructions, { type: if, condition: { type: exists, var: instructions },
125
+ then_expr: { type: var, name: instructions },
126
+ else_expr: { type: const, value: null } }]
127
+ - [max_depth, { type: if, condition: { type: exists, var: max_depth },
128
+ then_expr: { type: var, name: max_depth },
129
+ else_expr: { type: const, value: null } }]
130
+ - [max_breadth, { type: if, condition: { type: exists, var: max_breadth },
131
+ then_expr: { type: var, name: max_breadth },
132
+ else_expr: { type: const, value: null } }]
133
+ - [limit, { type: if, condition: { type: exists, var: limit },
134
+ then_expr: { type: var, name: limit },
135
+ else_expr: { type: const, value: null } }]
136
+ - [chunks_per_source, { type: if, condition: { type: exists, var: chunks_per_source },
137
+ then_expr: { type: var, name: chunks_per_source },
138
+ else_expr: { type: const, value: null } }]
139
+ - [include_images, { type: if, condition: { type: exists, var: include_images },
140
+ then_expr: { type: var, name: include_images },
141
+ else_expr: { type: const, value: null } }]
142
+
143
+ # ── Research ──────────────────────────────────────────────────────────────────
144
+
145
+ research_create:
146
+ # POST /research — async, returns {request_id, created_at, status, input, model}
147
+ # Explicit object body (not body: {type: var, name: input}): execute_create splats each
148
+ # param key into env, overwriting env["input"] with the scalar question string.
149
+ method: POST
150
+ path:
151
+ - { type: literal, value: research }
152
+ response: single
153
+ body:
154
+ type: object
155
+ fields:
156
+ - [input, { type: var, name: input }]
157
+ - [model, { type: if, condition: { type: exists, var: model },
158
+ then_expr: { type: var, name: model },
159
+ else_expr: { type: const, value: null } }]
160
+ - [citation_format, { type: if, condition: { type: exists, var: citation_format },
161
+ then_expr: { type: var, name: citation_format },
162
+ else_expr: { type: const, value: null } }]
163
+
164
+ research_get:
165
+ # GET /research/{request_id} — poll for task status and results
166
+ # id = request_id value from the ResearchTask entity
167
+ method: GET
168
+ path:
169
+ - { type: literal, value: research }
170
+ - { type: var, name: id }
171
+ response: single
@@ -0,0 +1,85 @@
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+
3
+ import { waitUntil } from "@vercel/functions";
4
+ import { defineChannel } from "@plasm_lang/vercel-agent";
5
+
6
+ import { readProofMarkdown } from "../../lib/proof-store.js";
7
+ import { radarStatus, runRadar } from "../../lib/run-radar.js";
8
+
9
+ function readJsonBody(req: IncomingMessage): Promise<unknown> {
10
+ return new Promise((resolve, reject) => {
11
+ let body = "";
12
+ req.on("data", (chunk) => {
13
+ body += chunk;
14
+ });
15
+ req.on("end", () => {
16
+ if (!body.trim()) {
17
+ resolve({});
18
+ return;
19
+ }
20
+ try {
21
+ resolve(JSON.parse(body));
22
+ } catch (err) {
23
+ reject(err);
24
+ }
25
+ });
26
+ req.on("error", reject);
27
+ });
28
+ }
29
+
30
+ function sendJson(res: ServerResponse, status: number, payload: unknown): void {
31
+ res.statusCode = status;
32
+ res.setHeader("content-type", "application/json; charset=utf-8");
33
+ res.end(JSON.stringify(payload, null, 2));
34
+ }
35
+
36
+ export default defineChannel({
37
+ name: "mcp-radar",
38
+ routes: [
39
+ {
40
+ method: "POST",
41
+ path: "/channel/mcp-radar/run",
42
+ handler: async (req, res, ctx) => {
43
+ const body = (await readJsonBody(req)) as { force?: boolean };
44
+ const options = { force: body.force === true };
45
+
46
+ if (process.env.VERCEL === "1") {
47
+ waitUntil(
48
+ runRadar(ctx, options).catch((err: unknown) => {
49
+ console.error("[mcp-radar] background run failed:", err);
50
+ }),
51
+ );
52
+ sendJson(res, 202, { accepted: true, async: true });
53
+ return;
54
+ }
55
+
56
+ const result = await runRadar(ctx, options);
57
+ const status = result.ok ? 200 : 500;
58
+ sendJson(res, status, result);
59
+ },
60
+ },
61
+ {
62
+ method: "GET",
63
+ path: "/channel/mcp-radar/proof",
64
+ handler: async (req, res, ctx) => {
65
+ const url = new URL(req.url ?? "/", "http://localhost");
66
+ const format = url.searchParams.get("format");
67
+ const markdown = await readProofMarkdown(ctx.agentRoot);
68
+ if (format === "json") {
69
+ sendJson(res, 200, { markdown, path: "agent/research/mcp-innovations-proof.md" });
70
+ return;
71
+ }
72
+ res.statusCode = 200;
73
+ res.setHeader("content-type", "text/markdown; charset=utf-8");
74
+ res.end(markdown);
75
+ },
76
+ },
77
+ {
78
+ method: "GET",
79
+ path: "/channel/mcp-radar/status",
80
+ handler: async (_req, res, ctx) => {
81
+ sendJson(res, 200, await radarStatus(ctx.agentRoot));
82
+ },
83
+ },
84
+ ],
85
+ });