@mindstudio-ai/remy 0.1.300 → 0.1.302
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/dist/headless.js +77 -6
- package/dist/index.js +85 -6
- package/dist/prompt/skills/dataSources.md +3 -3
- package/package.json +1 -1
package/dist/headless.js
CHANGED
|
@@ -2757,8 +2757,8 @@ function formatSize(bytes) {
|
|
|
2757
2757
|
}
|
|
2758
2758
|
async function formatFile(dirPath, name, indent) {
|
|
2759
2759
|
try {
|
|
2760
|
-
const
|
|
2761
|
-
return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(
|
|
2760
|
+
const stat4 = await fs16.stat(path8.join(dirPath, name));
|
|
2761
|
+
return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat4.size)}`;
|
|
2762
2762
|
} catch {
|
|
2763
2763
|
return `${indent}${name}`;
|
|
2764
2764
|
}
|
|
@@ -3787,6 +3787,7 @@ async function runSubAgent(config) {
|
|
|
3787
3787
|
acquireLock,
|
|
3788
3788
|
onBackgroundComplete,
|
|
3789
3789
|
captureArtifacts,
|
|
3790
|
+
validateResult,
|
|
3790
3791
|
cachePolicy = "run"
|
|
3791
3792
|
} = config;
|
|
3792
3793
|
const artifacts = {};
|
|
@@ -3807,6 +3808,7 @@ async function runSubAgent(config) {
|
|
|
3807
3808
|
|
|
3808
3809
|
Current date: ${dateStr}`;
|
|
3809
3810
|
let turns = 0;
|
|
3811
|
+
let validationRetried = false;
|
|
3810
3812
|
const run = async () => {
|
|
3811
3813
|
const historyLen = (history ?? []).length;
|
|
3812
3814
|
const subAgentMessages = /* @__PURE__ */ new Map();
|
|
@@ -4010,8 +4012,41 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
4010
4012
|
(b) => b.type === "tool"
|
|
4011
4013
|
);
|
|
4012
4014
|
if (stopReason !== "tool_use" || toolCalls.length === 0) {
|
|
4015
|
+
let text = getPartialText(contentBlocks);
|
|
4016
|
+
if (validateResult) {
|
|
4017
|
+
let objection = null;
|
|
4018
|
+
try {
|
|
4019
|
+
objection = await validateResult(text, thisInvocation());
|
|
4020
|
+
} catch (err) {
|
|
4021
|
+
log7.warn("Result validator failed, accepting response", {
|
|
4022
|
+
requestId,
|
|
4023
|
+
parentToolId,
|
|
4024
|
+
agentName,
|
|
4025
|
+
error: err.message
|
|
4026
|
+
});
|
|
4027
|
+
}
|
|
4028
|
+
if (objection && !validationRetried && !signal?.aborted) {
|
|
4029
|
+
validationRetried = true;
|
|
4030
|
+
log7.info("Result rejected by validator, retrying once", {
|
|
4031
|
+
requestId,
|
|
4032
|
+
parentToolId,
|
|
4033
|
+
agentName,
|
|
4034
|
+
objection: objection.slice(0, 200)
|
|
4035
|
+
});
|
|
4036
|
+
emit({
|
|
4037
|
+
type: "status",
|
|
4038
|
+
message: "Response failed validation, revising"
|
|
4039
|
+
});
|
|
4040
|
+
messages.push({ role: "user", content: objection });
|
|
4041
|
+
continue;
|
|
4042
|
+
}
|
|
4043
|
+
if (objection) {
|
|
4044
|
+
text = `${text}
|
|
4045
|
+
|
|
4046
|
+
[Response validator: ${objection}]`;
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4013
4049
|
statusWatcher.stop();
|
|
4014
|
-
const text = getPartialText(contentBlocks);
|
|
4015
4050
|
const hasArtifacts = Object.keys(artifacts).length > 0;
|
|
4016
4051
|
return {
|
|
4017
4052
|
text,
|
|
@@ -6055,6 +6090,38 @@ ${RENDER_TASK_BLOCK}`;
|
|
|
6055
6090
|
return prompt;
|
|
6056
6091
|
}
|
|
6057
6092
|
|
|
6093
|
+
// src/subagents/designExpert/validateWireframeRefs.ts
|
|
6094
|
+
import { stat as stat3 } from "fs/promises";
|
|
6095
|
+
import { join as join3 } from "path";
|
|
6096
|
+
var WIREFRAME_REF_RE = /src\/\.wireframes\/[a-z0-9][a-z0-9-]*\.html/g;
|
|
6097
|
+
async function validateWireframeRefs(text) {
|
|
6098
|
+
const refs = [...new Set(text.match(WIREFRAME_REF_RE) ?? [])];
|
|
6099
|
+
if (refs.length === 0) {
|
|
6100
|
+
return null;
|
|
6101
|
+
}
|
|
6102
|
+
const missing = [];
|
|
6103
|
+
for (const ref of refs) {
|
|
6104
|
+
const exists = await stat3(join3(PROJECT_ROOT, ref)).then(
|
|
6105
|
+
(s) => s.isFile(),
|
|
6106
|
+
() => false
|
|
6107
|
+
);
|
|
6108
|
+
if (!exists) {
|
|
6109
|
+
missing.push(ref);
|
|
6110
|
+
}
|
|
6111
|
+
}
|
|
6112
|
+
if (missing.length === 0) {
|
|
6113
|
+
return null;
|
|
6114
|
+
}
|
|
6115
|
+
return [
|
|
6116
|
+
`Your response references wireframe files that do not exist on disk:`,
|
|
6117
|
+
...missing.map((p) => `- ${p}`),
|
|
6118
|
+
``,
|
|
6119
|
+
`A wireframe reference is only valid as a receipt handed back by a createWireframe result \u2014 that tool is the only way a wireframe comes to exist. A reference composed in prose points at nothing and renders as a dead preview.`,
|
|
6120
|
+
``,
|
|
6121
|
+
`For each missing path, either author that wireframe now with createWireframe (use the matching slug so the path is identical) or remove the reference. Then send your complete final response again from the top \u2014 it fully replaces your previous response, so include everything, not just the fixes.`
|
|
6122
|
+
].join("\n");
|
|
6123
|
+
}
|
|
6124
|
+
|
|
6058
6125
|
// src/subagents/common/history.ts
|
|
6059
6126
|
function getSubAgentHistory(messages, subAgentName) {
|
|
6060
6127
|
let checkpointIdx = -1;
|
|
@@ -6143,6 +6210,10 @@ async function runDesignExpert(opts, context) {
|
|
|
6143
6210
|
onEvent: context.onEvent,
|
|
6144
6211
|
resolveExternalTool: context.resolveExternalTool,
|
|
6145
6212
|
toolRegistry: context.toolRegistry,
|
|
6213
|
+
// Receipt guard: every wireframe reference in the final response must
|
|
6214
|
+
// resolve to a file on disk (see validateWireframeRefs.ts). Covers the
|
|
6215
|
+
// advisor tool, render mode, and both render callers.
|
|
6216
|
+
validateResult: (text) => validateWireframeRefs(text),
|
|
6146
6217
|
background: opts.background,
|
|
6147
6218
|
onBackgroundComplete: opts.background ? (bgResult) => {
|
|
6148
6219
|
context.onBackgroundComplete?.(
|
|
@@ -9083,7 +9154,7 @@ async function runTurn(params) {
|
|
|
9083
9154
|
// src/headless/attachments.ts
|
|
9084
9155
|
import { mkdirSync, existsSync } from "fs";
|
|
9085
9156
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
9086
|
-
import { basename as basename2, join as
|
|
9157
|
+
import { basename as basename2, join as join4, extname as extname2 } from "path";
|
|
9087
9158
|
var log16 = createLogger("headless:attachments");
|
|
9088
9159
|
var UPLOADS_DIR = "src/.user-uploads";
|
|
9089
9160
|
function filenameFromUrl(url) {
|
|
@@ -9096,7 +9167,7 @@ function filenameFromUrl(url) {
|
|
|
9096
9167
|
}
|
|
9097
9168
|
}
|
|
9098
9169
|
function resolveUniqueFilename(name, claimed) {
|
|
9099
|
-
const isFree = (candidate) => !claimed.has(candidate) && !existsSync(
|
|
9170
|
+
const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join4(UPLOADS_DIR, candidate));
|
|
9100
9171
|
if (isFree(name)) {
|
|
9101
9172
|
return name;
|
|
9102
9173
|
}
|
|
@@ -9131,7 +9202,7 @@ async function persistAttachments(attachments) {
|
|
|
9131
9202
|
const results = await Promise.allSettled(
|
|
9132
9203
|
nonVoice.map(async (att, i) => {
|
|
9133
9204
|
const name = names[i];
|
|
9134
|
-
const localPath =
|
|
9205
|
+
const localPath = join4(UPLOADS_DIR, name);
|
|
9135
9206
|
const res = await fetch(att.url, {
|
|
9136
9207
|
signal: AbortSignal.timeout(3e4)
|
|
9137
9208
|
});
|
package/dist/index.js
CHANGED
|
@@ -3905,8 +3905,8 @@ function formatSize(bytes) {
|
|
|
3905
3905
|
}
|
|
3906
3906
|
async function formatFile(dirPath, name, indent) {
|
|
3907
3907
|
try {
|
|
3908
|
-
const
|
|
3909
|
-
return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(
|
|
3908
|
+
const stat4 = await fs15.stat(path8.join(dirPath, name));
|
|
3909
|
+
return `${indent}${name}${" ".repeat(Math.max(1, 30 - indent.length - name.length))}${formatSize(stat4.size)}`;
|
|
3910
3910
|
} catch {
|
|
3911
3911
|
return `${indent}${name}`;
|
|
3912
3912
|
}
|
|
@@ -4822,6 +4822,7 @@ async function runSubAgent(config) {
|
|
|
4822
4822
|
acquireLock,
|
|
4823
4823
|
onBackgroundComplete,
|
|
4824
4824
|
captureArtifacts,
|
|
4825
|
+
validateResult,
|
|
4825
4826
|
cachePolicy = "run"
|
|
4826
4827
|
} = config;
|
|
4827
4828
|
const artifacts = {};
|
|
@@ -4842,6 +4843,7 @@ async function runSubAgent(config) {
|
|
|
4842
4843
|
|
|
4843
4844
|
Current date: ${dateStr}`;
|
|
4844
4845
|
let turns = 0;
|
|
4846
|
+
let validationRetried = false;
|
|
4845
4847
|
const run = async () => {
|
|
4846
4848
|
const historyLen = (history ?? []).length;
|
|
4847
4849
|
const subAgentMessages = /* @__PURE__ */ new Map();
|
|
@@ -5045,8 +5047,41 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
5045
5047
|
(b) => b.type === "tool"
|
|
5046
5048
|
);
|
|
5047
5049
|
if (stopReason !== "tool_use" || toolCalls.length === 0) {
|
|
5050
|
+
let text = getPartialText(contentBlocks);
|
|
5051
|
+
if (validateResult) {
|
|
5052
|
+
let objection = null;
|
|
5053
|
+
try {
|
|
5054
|
+
objection = await validateResult(text, thisInvocation());
|
|
5055
|
+
} catch (err) {
|
|
5056
|
+
log8.warn("Result validator failed, accepting response", {
|
|
5057
|
+
requestId,
|
|
5058
|
+
parentToolId,
|
|
5059
|
+
agentName,
|
|
5060
|
+
error: err.message
|
|
5061
|
+
});
|
|
5062
|
+
}
|
|
5063
|
+
if (objection && !validationRetried && !signal?.aborted) {
|
|
5064
|
+
validationRetried = true;
|
|
5065
|
+
log8.info("Result rejected by validator, retrying once", {
|
|
5066
|
+
requestId,
|
|
5067
|
+
parentToolId,
|
|
5068
|
+
agentName,
|
|
5069
|
+
objection: objection.slice(0, 200)
|
|
5070
|
+
});
|
|
5071
|
+
emit({
|
|
5072
|
+
type: "status",
|
|
5073
|
+
message: "Response failed validation, revising"
|
|
5074
|
+
});
|
|
5075
|
+
messages.push({ role: "user", content: objection });
|
|
5076
|
+
continue;
|
|
5077
|
+
}
|
|
5078
|
+
if (objection) {
|
|
5079
|
+
text = `${text}
|
|
5080
|
+
|
|
5081
|
+
[Response validator: ${objection}]`;
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5048
5084
|
statusWatcher.stop();
|
|
5049
|
-
const text = getPartialText(contentBlocks);
|
|
5050
5085
|
const hasArtifacts = Object.keys(artifacts).length > 0;
|
|
5051
5086
|
return {
|
|
5052
5087
|
text,
|
|
@@ -7413,6 +7448,45 @@ The guidance about specifying layouts in prose, writing implementation notes, an
|
|
|
7413
7448
|
}
|
|
7414
7449
|
});
|
|
7415
7450
|
|
|
7451
|
+
// src/subagents/designExpert/validateWireframeRefs.ts
|
|
7452
|
+
import { stat as stat3 } from "fs/promises";
|
|
7453
|
+
import { join as join3 } from "path";
|
|
7454
|
+
async function validateWireframeRefs(text) {
|
|
7455
|
+
const refs = [...new Set(text.match(WIREFRAME_REF_RE) ?? [])];
|
|
7456
|
+
if (refs.length === 0) {
|
|
7457
|
+
return null;
|
|
7458
|
+
}
|
|
7459
|
+
const missing = [];
|
|
7460
|
+
for (const ref of refs) {
|
|
7461
|
+
const exists = await stat3(join3(PROJECT_ROOT, ref)).then(
|
|
7462
|
+
(s) => s.isFile(),
|
|
7463
|
+
() => false
|
|
7464
|
+
);
|
|
7465
|
+
if (!exists) {
|
|
7466
|
+
missing.push(ref);
|
|
7467
|
+
}
|
|
7468
|
+
}
|
|
7469
|
+
if (missing.length === 0) {
|
|
7470
|
+
return null;
|
|
7471
|
+
}
|
|
7472
|
+
return [
|
|
7473
|
+
`Your response references wireframe files that do not exist on disk:`,
|
|
7474
|
+
...missing.map((p) => `- ${p}`),
|
|
7475
|
+
``,
|
|
7476
|
+
`A wireframe reference is only valid as a receipt handed back by a createWireframe result \u2014 that tool is the only way a wireframe comes to exist. A reference composed in prose points at nothing and renders as a dead preview.`,
|
|
7477
|
+
``,
|
|
7478
|
+
`For each missing path, either author that wireframe now with createWireframe (use the matching slug so the path is identical) or remove the reference. Then send your complete final response again from the top \u2014 it fully replaces your previous response, so include everything, not just the fixes.`
|
|
7479
|
+
].join("\n");
|
|
7480
|
+
}
|
|
7481
|
+
var WIREFRAME_REF_RE;
|
|
7482
|
+
var init_validateWireframeRefs = __esm({
|
|
7483
|
+
"src/subagents/designExpert/validateWireframeRefs.ts"() {
|
|
7484
|
+
"use strict";
|
|
7485
|
+
init_projectRoot();
|
|
7486
|
+
WIREFRAME_REF_RE = /src\/\.wireframes\/[a-z0-9][a-z0-9-]*\.html/g;
|
|
7487
|
+
}
|
|
7488
|
+
});
|
|
7489
|
+
|
|
7416
7490
|
// src/subagents/common/history.ts
|
|
7417
7491
|
function getSubAgentHistory(messages, subAgentName) {
|
|
7418
7492
|
let checkpointIdx = -1;
|
|
@@ -7497,6 +7571,10 @@ async function runDesignExpert(opts, context) {
|
|
|
7497
7571
|
onEvent: context.onEvent,
|
|
7498
7572
|
resolveExternalTool: context.resolveExternalTool,
|
|
7499
7573
|
toolRegistry: context.toolRegistry,
|
|
7574
|
+
// Receipt guard: every wireframe reference in the final response must
|
|
7575
|
+
// resolve to a file on disk (see validateWireframeRefs.ts). Covers the
|
|
7576
|
+
// advisor tool, render mode, and both render callers.
|
|
7577
|
+
validateResult: (text) => validateWireframeRefs(text),
|
|
7500
7578
|
background: opts.background,
|
|
7501
7579
|
onBackgroundComplete: opts.background ? (bgResult) => {
|
|
7502
7580
|
context.onBackgroundComplete?.(
|
|
@@ -7520,6 +7598,7 @@ var init_designExpert = __esm({
|
|
|
7520
7598
|
init_tools4();
|
|
7521
7599
|
init_tools();
|
|
7522
7600
|
init_prompt2();
|
|
7601
|
+
init_validateWireframeRefs();
|
|
7523
7602
|
init_history();
|
|
7524
7603
|
init_surfaces();
|
|
7525
7604
|
init_writeFile();
|
|
@@ -10011,7 +10090,7 @@ var init_config = __esm({
|
|
|
10011
10090
|
// src/headless/attachments.ts
|
|
10012
10091
|
import { mkdirSync, existsSync } from "fs";
|
|
10013
10092
|
import { writeFile as writeFile3 } from "fs/promises";
|
|
10014
|
-
import { basename as basename2, join as
|
|
10093
|
+
import { basename as basename2, join as join4, extname as extname2 } from "path";
|
|
10015
10094
|
function filenameFromUrl(url) {
|
|
10016
10095
|
try {
|
|
10017
10096
|
const pathname = new URL(url).pathname;
|
|
@@ -10022,7 +10101,7 @@ function filenameFromUrl(url) {
|
|
|
10022
10101
|
}
|
|
10023
10102
|
}
|
|
10024
10103
|
function resolveUniqueFilename(name, claimed) {
|
|
10025
|
-
const isFree = (candidate) => !claimed.has(candidate) && !existsSync(
|
|
10104
|
+
const isFree = (candidate) => !claimed.has(candidate) && !existsSync(join4(UPLOADS_DIR, candidate));
|
|
10026
10105
|
if (isFree(name)) {
|
|
10027
10106
|
return name;
|
|
10028
10107
|
}
|
|
@@ -10056,7 +10135,7 @@ async function persistAttachments(attachments) {
|
|
|
10056
10135
|
const results = await Promise.allSettled(
|
|
10057
10136
|
nonVoice.map(async (att, i) => {
|
|
10058
10137
|
const name = names[i];
|
|
10059
|
-
const localPath =
|
|
10138
|
+
const localPath = join4(UPLOADS_DIR, name);
|
|
10060
10139
|
const res = await fetch(att.url, {
|
|
10061
10140
|
signal: AbortSignal.timeout(3e4)
|
|
10062
10141
|
});
|
|
@@ -31,13 +31,13 @@ const { results } = await Policies.search('what are the payment terms?', { topK:
|
|
|
31
31
|
const context = results.map((r) => r.text).join('\n\n');
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
Hits are `{ score, text, citation }` with `citation: { documentId, filename, pageNumber, chunkIndex, headingPath, boundingBox?, url }`, plus `retrievalRank`/`retrievalScore` — the position before reranking, so you can show what reranking did.
|
|
34
|
+
Hits are `{ score, text, citation }` with `citation: { documentId, filename, pageNumber, chunkIndex, headingPath, boundingBox?, url }`, plus `retrievalRank`/`retrievalScore` — the position before reranking, so you can show what reranking did. With reranking on (the default) `score` is the reranker's 0–1 relevance and the right place for quality cutoffs; without it the scale varies by mode (cosine / rank-fusion / keyword overlap). `scoreThreshold` floors the retrieval branch before fusion and reranking — leave it unset unless measured on the corpus.
|
|
35
35
|
|
|
36
36
|
**Always render the citation.** `citation.url` is a stable on-domain link — put it in an `<a href>` beside the answer. Retrieval is approximate; a user who can click through can judge for themselves. An answer with no citation is an assertion.
|
|
37
37
|
|
|
38
38
|
Created on first use, so searching a source the build hasn't populated returns no results rather than throwing. `search` options: `topK` (default 5, max 50), `scoreThreshold`, `filter`, `mode`, `maxPerDocument`, `highlight`, `rerank`, `hybrid`.
|
|
39
39
|
|
|
40
|
-
**Filtering** narrows a search before ranking, and every condition only narrows: `filter: { metadata: { department: 'legal', year: [2025, 2026] }, filename, documentIds, pages: { min?, max? }, contains: 'all these words', phrase: 'exact adjacent sequence' }`. Metadata is tagged at add time (scalars only, ≤16 keys); re-adding the same bytes with different metadata updates the tags in place, free. Filters are the right tool for scoping retrieval (per-user, per-category); they are NOT a substitute for a `db` query over structured data.
|
|
40
|
+
**Filtering** narrows a search before ranking, and every condition only narrows: `filter: { metadata: { department: 'legal', year: [2025, 2026], signedAt: { gte: 20250101 } }, filename, documentIds, pages: { min?, max? }, contains: 'all these words', phrase: 'exact adjacent sequence' }`. Metadata matches per key: scalar = equals, array = any-of, `{ gte?, lte? }` = numeric range — ranges are numeric only, so store dates as sortable integers at add time (YYYYMMDD or epoch seconds) to range on them. Metadata is tagged at add time (scalars only, ≤16 keys); re-adding the same bytes with different metadata updates the tags in place, free. Filters are the right tool for scoping retrieval (per-user, per-category, a date window); they are NOT a substitute for a `db` query over structured data.
|
|
41
41
|
|
|
42
42
|
**Modes**: `mode: 'hybrid'` (default) fuses semantic and keyword retrieval; `'semantic'` is the embedding alone; `'lexical'` is keyword-only with **no query embedding** — cheapest and fastest, right when the query is an identifier (an error code, a SKU, a name) rather than a meaning. `maxPerDocument: 2` stops one document monopolizing the results when the answer should draw on several. `highlight: true` adds `matches` (`{start, end}` offsets into `text`) for rendering highlighted excerpts.
|
|
43
43
|
|
|
@@ -84,7 +84,7 @@ Retrieve → join passages as context → have a model answer *from that context
|
|
|
84
84
|
| Kind | Settings | Cost |
|
|
85
85
|
|---|---|---|
|
|
86
86
|
| **Free** (ranking) | `--rerank`, `--rerank-model`, `--hybrid`, `--top-k` | none, next search |
|
|
87
|
-
| **Rebuild** (how docs become vectors) | `--max-chars`, `--min-chars`, `--drop-blocks`, `--contextual`, `--describe-images`, `--embedding-model`, `--extraction-model` | every document reprocessed |
|
|
87
|
+
| **Rebuild** (how docs become vectors) | `--max-chars`, `--min-chars`, `--drop-blocks`, `--contextual`, `--contextual-model`, `--describe-images`, `--embedding-model`, `--extraction-model` | every document reprocessed |
|
|
88
88
|
|
|
89
89
|
Images inside documents are described by a vision model and the description substituted into the searchable text (`--describe-images`, on by default) — without it a chart contributes nothing to search at all. Documents with no images cost nothing.
|
|
90
90
|
|