@mindstudio-ai/remy 0.1.306 → 0.1.308
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
CHANGED
|
@@ -3009,7 +3009,7 @@ var runScenarioTool = {
|
|
|
3009
3009
|
var runMethodTool = {
|
|
3010
3010
|
definition: {
|
|
3011
3011
|
name: "runMethod",
|
|
3012
|
-
description: 'Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Returns synchronously \u2014 no need to sleep before checking results.\n\nBy default methods run unauthenticated. If the method is auth-gated (calls `auth.requireRole()`, filters on `auth.userId`, etc.), pass `userId: "testUser"` to run as the default test user \u2014 no scenario setup required, no userId lookup.',
|
|
3012
|
+
description: 'Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Returns synchronously \u2014 no need to sleep before checking results.\n\nBy default methods run unauthenticated. If the method is auth-gated (calls `auth.requireRole()`, filters on `auth.userId`, etc.), pass `userId: "testUser"` to run as the default test user \u2014 no scenario setup required, no userId lookup. For a method gated on `auth.requireRole("system")` \u2014 cron, webhook, and email work \u2014 pass `roles: ["system"]`; that works whether or not the app has auth.',
|
|
3013
3013
|
inputSchema: {
|
|
3014
3014
|
type: "object",
|
|
3015
3015
|
properties: {
|
|
@@ -3028,7 +3028,7 @@ var runMethodTool = {
|
|
|
3028
3028
|
roles: {
|
|
3029
3029
|
type: "array",
|
|
3030
3030
|
items: { type: "string" },
|
|
3031
|
-
description: 'Optional. Role names for this request (e.g. ["admin"]).
|
|
3031
|
+
description: 'Optional. Role names for this request (e.g. ["admin"]). Roles without a userId bind to the dev test user holding exactly these roles \u2014 a real user row, so `auth.userId` and `requireRole` behave like production. `["system"]` is the exception and needs no user: it runs as the platform system identity, the same one cron, webhook, and email invocations get, so a system-gated method is testable in an app with no auth configured. Any other role requires an `auth` block in mindstudio.json \u2014 without one the app has no users to hold a role, and the call is rejected saying so. Applies to this call only.'
|
|
3032
3032
|
}
|
|
3033
3033
|
},
|
|
3034
3034
|
required: ["method"]
|
|
@@ -8444,6 +8444,155 @@ function friendlyError(raw) {
|
|
|
8444
8444
|
return `Something went wrong: ${raw}`;
|
|
8445
8445
|
}
|
|
8446
8446
|
|
|
8447
|
+
// src/suggestions.ts
|
|
8448
|
+
var SUGGEST_MARKER = "](suggest:";
|
|
8449
|
+
var SEPARATORS = "\\s\xB7|,\\-\u2013\u2014";
|
|
8450
|
+
var SEPARATORS_ONLY = new RegExp(`^[${SEPARATORS}]*$`);
|
|
8451
|
+
var TRAILING_SEPARATORS = new RegExp(`[${SEPARATORS}]+$`);
|
|
8452
|
+
var LIST_MARKER = /^\s*(?:[-*+]|\d+[.)])\s+/;
|
|
8453
|
+
var FENCE = /^ {0,3}(?:```|~~~)/;
|
|
8454
|
+
function findLinks(line) {
|
|
8455
|
+
const found = [];
|
|
8456
|
+
let i = 0;
|
|
8457
|
+
while (i < line.length) {
|
|
8458
|
+
const ch = line[i];
|
|
8459
|
+
if (ch === "\\") {
|
|
8460
|
+
i += 2;
|
|
8461
|
+
continue;
|
|
8462
|
+
}
|
|
8463
|
+
if (ch === "`") {
|
|
8464
|
+
let runEnd = i;
|
|
8465
|
+
while (runEnd < line.length && line[runEnd] === "`") {
|
|
8466
|
+
runEnd++;
|
|
8467
|
+
}
|
|
8468
|
+
const fence = line.slice(i, runEnd);
|
|
8469
|
+
const close = line.indexOf(fence, runEnd);
|
|
8470
|
+
if (close === -1) {
|
|
8471
|
+
break;
|
|
8472
|
+
}
|
|
8473
|
+
i = close + fence.length;
|
|
8474
|
+
continue;
|
|
8475
|
+
}
|
|
8476
|
+
if (ch !== "[") {
|
|
8477
|
+
i++;
|
|
8478
|
+
continue;
|
|
8479
|
+
}
|
|
8480
|
+
const labelEnd = line.indexOf("]", i + 1);
|
|
8481
|
+
if (labelEnd === -1) {
|
|
8482
|
+
break;
|
|
8483
|
+
}
|
|
8484
|
+
if (!line.startsWith(SUGGEST_MARKER, labelEnd)) {
|
|
8485
|
+
i = labelEnd + 1;
|
|
8486
|
+
continue;
|
|
8487
|
+
}
|
|
8488
|
+
let depth = 1;
|
|
8489
|
+
let p = labelEnd + SUGGEST_MARKER.length;
|
|
8490
|
+
while (p < line.length && depth > 0) {
|
|
8491
|
+
if (line[p] === "(") {
|
|
8492
|
+
depth++;
|
|
8493
|
+
} else if (line[p] === ")") {
|
|
8494
|
+
depth--;
|
|
8495
|
+
}
|
|
8496
|
+
p++;
|
|
8497
|
+
}
|
|
8498
|
+
if (depth > 0) {
|
|
8499
|
+
break;
|
|
8500
|
+
}
|
|
8501
|
+
found.push({
|
|
8502
|
+
start: i,
|
|
8503
|
+
end: p,
|
|
8504
|
+
label: line.slice(i + 1, labelEnd).trim(),
|
|
8505
|
+
message: line.slice(labelEnd + SUGGEST_MARKER.length, p - 1).trim(),
|
|
8506
|
+
standalone: false
|
|
8507
|
+
});
|
|
8508
|
+
i = p;
|
|
8509
|
+
}
|
|
8510
|
+
return found;
|
|
8511
|
+
}
|
|
8512
|
+
function classify(line, matches) {
|
|
8513
|
+
let tailClean = SEPARATORS_ONLY.test(
|
|
8514
|
+
line.slice(matches[matches.length - 1].end)
|
|
8515
|
+
);
|
|
8516
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
8517
|
+
matches[i].standalone = tailClean;
|
|
8518
|
+
if (!tailClean) {
|
|
8519
|
+
continue;
|
|
8520
|
+
}
|
|
8521
|
+
const gap = i > 0 ? line.slice(matches[i - 1].end, matches[i].start) : "";
|
|
8522
|
+
tailClean = SEPARATORS_ONLY.test(gap);
|
|
8523
|
+
}
|
|
8524
|
+
}
|
|
8525
|
+
function parseSuggestions(raw) {
|
|
8526
|
+
if (!raw.includes(SUGGEST_MARKER)) {
|
|
8527
|
+
return { text: raw, suggestions: [] };
|
|
8528
|
+
}
|
|
8529
|
+
const suggestions = [];
|
|
8530
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8531
|
+
const outLines = [];
|
|
8532
|
+
let inFence = false;
|
|
8533
|
+
let droppedLine = false;
|
|
8534
|
+
for (const line of raw.split("\n")) {
|
|
8535
|
+
if (FENCE.test(line)) {
|
|
8536
|
+
inFence = !inFence;
|
|
8537
|
+
outLines.push(line);
|
|
8538
|
+
continue;
|
|
8539
|
+
}
|
|
8540
|
+
if (inFence) {
|
|
8541
|
+
outLines.push(line);
|
|
8542
|
+
continue;
|
|
8543
|
+
}
|
|
8544
|
+
const matches = findLinks(line);
|
|
8545
|
+
if (matches.length === 0) {
|
|
8546
|
+
outLines.push(line);
|
|
8547
|
+
continue;
|
|
8548
|
+
}
|
|
8549
|
+
classify(line, matches);
|
|
8550
|
+
for (const m of matches) {
|
|
8551
|
+
const key = `${m.label}::${m.message}`;
|
|
8552
|
+
if (!seen.has(key)) {
|
|
8553
|
+
seen.add(key);
|
|
8554
|
+
suggestions.push({ label: m.label, message: m.message });
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
let rebuilt = "";
|
|
8558
|
+
let cursor = 0;
|
|
8559
|
+
for (const m of matches) {
|
|
8560
|
+
rebuilt += line.slice(cursor, m.start);
|
|
8561
|
+
if (!m.standalone) {
|
|
8562
|
+
rebuilt += m.label;
|
|
8563
|
+
}
|
|
8564
|
+
cursor = m.end;
|
|
8565
|
+
}
|
|
8566
|
+
rebuilt += line.slice(cursor);
|
|
8567
|
+
if (matches.some((m) => m.standalone)) {
|
|
8568
|
+
if (SEPARATORS_ONLY.test(rebuilt.replace(LIST_MARKER, ""))) {
|
|
8569
|
+
droppedLine = true;
|
|
8570
|
+
continue;
|
|
8571
|
+
}
|
|
8572
|
+
rebuilt = rebuilt.replace(TRAILING_SEPARATORS, "");
|
|
8573
|
+
}
|
|
8574
|
+
outLines.push(rebuilt);
|
|
8575
|
+
}
|
|
8576
|
+
let text = outLines.join("\n").replace(/\s+$/, "");
|
|
8577
|
+
if (droppedLine) {
|
|
8578
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
8579
|
+
}
|
|
8580
|
+
return { text, suggestions };
|
|
8581
|
+
}
|
|
8582
|
+
function annotateSuggestions(blocks) {
|
|
8583
|
+
for (const block of blocks) {
|
|
8584
|
+
if (block.type !== "text" || !block.text.includes(SUGGEST_MARKER)) {
|
|
8585
|
+
continue;
|
|
8586
|
+
}
|
|
8587
|
+
const { text, suggestions } = parseSuggestions(block.text);
|
|
8588
|
+
if (suggestions.length === 0) {
|
|
8589
|
+
continue;
|
|
8590
|
+
}
|
|
8591
|
+
block.displayText = text;
|
|
8592
|
+
block.suggestions = suggestions;
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
8595
|
+
|
|
8447
8596
|
// src/agent.ts
|
|
8448
8597
|
var log15 = createLogger("agent");
|
|
8449
8598
|
var BRAND_TRIGGERING_TOOLS = /* @__PURE__ */ new Set([
|
|
@@ -8638,8 +8787,27 @@ async function runTurn(params) {
|
|
|
8638
8787
|
toolInputAccumulators.set(id, acc);
|
|
8639
8788
|
}
|
|
8640
8789
|
return acc;
|
|
8790
|
+
}, emitTextBlockSnapshot2 = function(closing) {
|
|
8791
|
+
let block;
|
|
8792
|
+
for (let i = contentBlocks.length - 1; i >= 0; i--) {
|
|
8793
|
+
const candidate = contentBlocks[i];
|
|
8794
|
+
if (candidate.type === "text") {
|
|
8795
|
+
block = candidate;
|
|
8796
|
+
break;
|
|
8797
|
+
}
|
|
8798
|
+
}
|
|
8799
|
+
if (!block || !block.text.includes(SUGGEST_MARKER)) {
|
|
8800
|
+
return;
|
|
8801
|
+
}
|
|
8802
|
+
const { text, suggestions } = parseSuggestions(block.text);
|
|
8803
|
+
const worthSending = closing ? suggestions.length > 0 : suggestions.length > emittedSuggestionCount;
|
|
8804
|
+
if (!worthSending) {
|
|
8805
|
+
return;
|
|
8806
|
+
}
|
|
8807
|
+
emittedSuggestionCount = suggestions.length;
|
|
8808
|
+
onEvent({ type: "text_block", text, suggestions });
|
|
8641
8809
|
};
|
|
8642
|
-
var getOrCreateAccumulator = getOrCreateAccumulator2;
|
|
8810
|
+
var getOrCreateAccumulator = getOrCreateAccumulator2, emitTextBlockSnapshot = emitTextBlockSnapshot2;
|
|
8643
8811
|
if (signal?.aborted) {
|
|
8644
8812
|
onEvent({ type: "turn_cancelled" });
|
|
8645
8813
|
saveSession(state);
|
|
@@ -8651,6 +8819,7 @@ async function runTurn(params) {
|
|
|
8651
8819
|
let thinkingCompleteCount = 0;
|
|
8652
8820
|
let lastThinkingRelatedStartedAt;
|
|
8653
8821
|
let textBlockOpen = false;
|
|
8822
|
+
let emittedSuggestionCount = 0;
|
|
8654
8823
|
const toolInputAccumulators = /* @__PURE__ */ new Map();
|
|
8655
8824
|
let stopReason = "end_turn";
|
|
8656
8825
|
let turnProviderMetadata;
|
|
@@ -8739,14 +8908,19 @@ async function runTurn(params) {
|
|
|
8739
8908
|
text: event.text,
|
|
8740
8909
|
startedAt: event.ts
|
|
8741
8910
|
});
|
|
8911
|
+
emittedSuggestionCount = 0;
|
|
8742
8912
|
}
|
|
8743
8913
|
textBlockOpen = true;
|
|
8744
8914
|
onEvent({ type: "text", text: event.text });
|
|
8915
|
+
emitTextBlockSnapshot2(false);
|
|
8745
8916
|
break;
|
|
8746
8917
|
}
|
|
8747
8918
|
case "thinking":
|
|
8748
8919
|
if (event.text === "") {
|
|
8749
8920
|
thinkingBlockStartTimes.push(event.ts);
|
|
8921
|
+
if (textBlockOpen) {
|
|
8922
|
+
emitTextBlockSnapshot2(true);
|
|
8923
|
+
}
|
|
8750
8924
|
textBlockOpen = false;
|
|
8751
8925
|
}
|
|
8752
8926
|
onEvent({ type: "thinking", text: event.text });
|
|
@@ -8872,6 +9046,11 @@ async function runTurn(params) {
|
|
|
8872
9046
|
throw err;
|
|
8873
9047
|
}
|
|
8874
9048
|
}
|
|
9049
|
+
if (textBlockOpen) {
|
|
9050
|
+
emitTextBlockSnapshot2(true);
|
|
9051
|
+
textBlockOpen = false;
|
|
9052
|
+
}
|
|
9053
|
+
annotateSuggestions(contentBlocks);
|
|
8875
9054
|
if (signal?.aborted) {
|
|
8876
9055
|
statusWatcher.stop();
|
|
8877
9056
|
if (contentBlocks.length > 0) {
|
|
@@ -10002,6 +10181,13 @@ var HeadlessSession = class {
|
|
|
10002
10181
|
rid
|
|
10003
10182
|
);
|
|
10004
10183
|
return;
|
|
10184
|
+
case "text_block":
|
|
10185
|
+
this.emit(
|
|
10186
|
+
"text_block",
|
|
10187
|
+
{ text: e.text, suggestions: e.suggestions },
|
|
10188
|
+
rid
|
|
10189
|
+
);
|
|
10190
|
+
return;
|
|
10005
10191
|
case "thinking":
|
|
10006
10192
|
this.emit(
|
|
10007
10193
|
"thinking",
|
package/dist/index.js
CHANGED
|
@@ -4200,7 +4200,7 @@ var init_runMethod = __esm({
|
|
|
4200
4200
|
runMethodTool = {
|
|
4201
4201
|
definition: {
|
|
4202
4202
|
name: "runMethod",
|
|
4203
|
-
description: 'Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Returns synchronously \u2014 no need to sleep before checking results.\n\nBy default methods run unauthenticated. If the method is auth-gated (calls `auth.requireRole()`, filters on `auth.userId`, etc.), pass `userId: "testUser"` to run as the default test user \u2014 no scenario setup required, no userId lookup.',
|
|
4203
|
+
description: 'Run a method in the dev environment and return the result. Use for testing methods after writing or modifying them. Returns output, captured console output, errors with stack traces, and duration. If it fails, check .logs/tunnel.log or .logs/requests.ndjson for more details. Returns synchronously \u2014 no need to sleep before checking results.\n\nBy default methods run unauthenticated. If the method is auth-gated (calls `auth.requireRole()`, filters on `auth.userId`, etc.), pass `userId: "testUser"` to run as the default test user \u2014 no scenario setup required, no userId lookup. For a method gated on `auth.requireRole("system")` \u2014 cron, webhook, and email work \u2014 pass `roles: ["system"]`; that works whether or not the app has auth.',
|
|
4204
4204
|
inputSchema: {
|
|
4205
4205
|
type: "object",
|
|
4206
4206
|
properties: {
|
|
@@ -4219,7 +4219,7 @@ var init_runMethod = __esm({
|
|
|
4219
4219
|
roles: {
|
|
4220
4220
|
type: "array",
|
|
4221
4221
|
items: { type: "string" },
|
|
4222
|
-
description: 'Optional. Role names for this request (e.g. ["admin"]).
|
|
4222
|
+
description: 'Optional. Role names for this request (e.g. ["admin"]). Roles without a userId bind to the dev test user holding exactly these roles \u2014 a real user row, so `auth.userId` and `requireRole` behave like production. `["system"]` is the exception and needs no user: it runs as the platform system identity, the same one cron, webhook, and email invocations get, so a system-gated method is testable in an app with no auth configured. Any other role requires an `auth` block in mindstudio.json \u2014 without one the app has no users to hold a role, and the call is rejected saying so. Applies to this call only.'
|
|
4223
4223
|
}
|
|
4224
4224
|
},
|
|
4225
4225
|
required: ["method"]
|
|
@@ -8752,6 +8752,161 @@ var init_errors = __esm({
|
|
|
8752
8752
|
}
|
|
8753
8753
|
});
|
|
8754
8754
|
|
|
8755
|
+
// src/suggestions.ts
|
|
8756
|
+
function findLinks(line) {
|
|
8757
|
+
const found = [];
|
|
8758
|
+
let i = 0;
|
|
8759
|
+
while (i < line.length) {
|
|
8760
|
+
const ch = line[i];
|
|
8761
|
+
if (ch === "\\") {
|
|
8762
|
+
i += 2;
|
|
8763
|
+
continue;
|
|
8764
|
+
}
|
|
8765
|
+
if (ch === "`") {
|
|
8766
|
+
let runEnd = i;
|
|
8767
|
+
while (runEnd < line.length && line[runEnd] === "`") {
|
|
8768
|
+
runEnd++;
|
|
8769
|
+
}
|
|
8770
|
+
const fence = line.slice(i, runEnd);
|
|
8771
|
+
const close = line.indexOf(fence, runEnd);
|
|
8772
|
+
if (close === -1) {
|
|
8773
|
+
break;
|
|
8774
|
+
}
|
|
8775
|
+
i = close + fence.length;
|
|
8776
|
+
continue;
|
|
8777
|
+
}
|
|
8778
|
+
if (ch !== "[") {
|
|
8779
|
+
i++;
|
|
8780
|
+
continue;
|
|
8781
|
+
}
|
|
8782
|
+
const labelEnd = line.indexOf("]", i + 1);
|
|
8783
|
+
if (labelEnd === -1) {
|
|
8784
|
+
break;
|
|
8785
|
+
}
|
|
8786
|
+
if (!line.startsWith(SUGGEST_MARKER, labelEnd)) {
|
|
8787
|
+
i = labelEnd + 1;
|
|
8788
|
+
continue;
|
|
8789
|
+
}
|
|
8790
|
+
let depth = 1;
|
|
8791
|
+
let p = labelEnd + SUGGEST_MARKER.length;
|
|
8792
|
+
while (p < line.length && depth > 0) {
|
|
8793
|
+
if (line[p] === "(") {
|
|
8794
|
+
depth++;
|
|
8795
|
+
} else if (line[p] === ")") {
|
|
8796
|
+
depth--;
|
|
8797
|
+
}
|
|
8798
|
+
p++;
|
|
8799
|
+
}
|
|
8800
|
+
if (depth > 0) {
|
|
8801
|
+
break;
|
|
8802
|
+
}
|
|
8803
|
+
found.push({
|
|
8804
|
+
start: i,
|
|
8805
|
+
end: p,
|
|
8806
|
+
label: line.slice(i + 1, labelEnd).trim(),
|
|
8807
|
+
message: line.slice(labelEnd + SUGGEST_MARKER.length, p - 1).trim(),
|
|
8808
|
+
standalone: false
|
|
8809
|
+
});
|
|
8810
|
+
i = p;
|
|
8811
|
+
}
|
|
8812
|
+
return found;
|
|
8813
|
+
}
|
|
8814
|
+
function classify(line, matches) {
|
|
8815
|
+
let tailClean = SEPARATORS_ONLY.test(
|
|
8816
|
+
line.slice(matches[matches.length - 1].end)
|
|
8817
|
+
);
|
|
8818
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
8819
|
+
matches[i].standalone = tailClean;
|
|
8820
|
+
if (!tailClean) {
|
|
8821
|
+
continue;
|
|
8822
|
+
}
|
|
8823
|
+
const gap = i > 0 ? line.slice(matches[i - 1].end, matches[i].start) : "";
|
|
8824
|
+
tailClean = SEPARATORS_ONLY.test(gap);
|
|
8825
|
+
}
|
|
8826
|
+
}
|
|
8827
|
+
function parseSuggestions(raw) {
|
|
8828
|
+
if (!raw.includes(SUGGEST_MARKER)) {
|
|
8829
|
+
return { text: raw, suggestions: [] };
|
|
8830
|
+
}
|
|
8831
|
+
const suggestions = [];
|
|
8832
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8833
|
+
const outLines = [];
|
|
8834
|
+
let inFence = false;
|
|
8835
|
+
let droppedLine = false;
|
|
8836
|
+
for (const line of raw.split("\n")) {
|
|
8837
|
+
if (FENCE.test(line)) {
|
|
8838
|
+
inFence = !inFence;
|
|
8839
|
+
outLines.push(line);
|
|
8840
|
+
continue;
|
|
8841
|
+
}
|
|
8842
|
+
if (inFence) {
|
|
8843
|
+
outLines.push(line);
|
|
8844
|
+
continue;
|
|
8845
|
+
}
|
|
8846
|
+
const matches = findLinks(line);
|
|
8847
|
+
if (matches.length === 0) {
|
|
8848
|
+
outLines.push(line);
|
|
8849
|
+
continue;
|
|
8850
|
+
}
|
|
8851
|
+
classify(line, matches);
|
|
8852
|
+
for (const m of matches) {
|
|
8853
|
+
const key = `${m.label}::${m.message}`;
|
|
8854
|
+
if (!seen.has(key)) {
|
|
8855
|
+
seen.add(key);
|
|
8856
|
+
suggestions.push({ label: m.label, message: m.message });
|
|
8857
|
+
}
|
|
8858
|
+
}
|
|
8859
|
+
let rebuilt = "";
|
|
8860
|
+
let cursor = 0;
|
|
8861
|
+
for (const m of matches) {
|
|
8862
|
+
rebuilt += line.slice(cursor, m.start);
|
|
8863
|
+
if (!m.standalone) {
|
|
8864
|
+
rebuilt += m.label;
|
|
8865
|
+
}
|
|
8866
|
+
cursor = m.end;
|
|
8867
|
+
}
|
|
8868
|
+
rebuilt += line.slice(cursor);
|
|
8869
|
+
if (matches.some((m) => m.standalone)) {
|
|
8870
|
+
if (SEPARATORS_ONLY.test(rebuilt.replace(LIST_MARKER, ""))) {
|
|
8871
|
+
droppedLine = true;
|
|
8872
|
+
continue;
|
|
8873
|
+
}
|
|
8874
|
+
rebuilt = rebuilt.replace(TRAILING_SEPARATORS, "");
|
|
8875
|
+
}
|
|
8876
|
+
outLines.push(rebuilt);
|
|
8877
|
+
}
|
|
8878
|
+
let text = outLines.join("\n").replace(/\s+$/, "");
|
|
8879
|
+
if (droppedLine) {
|
|
8880
|
+
text = text.replace(/\n{3,}/g, "\n\n");
|
|
8881
|
+
}
|
|
8882
|
+
return { text, suggestions };
|
|
8883
|
+
}
|
|
8884
|
+
function annotateSuggestions(blocks) {
|
|
8885
|
+
for (const block of blocks) {
|
|
8886
|
+
if (block.type !== "text" || !block.text.includes(SUGGEST_MARKER)) {
|
|
8887
|
+
continue;
|
|
8888
|
+
}
|
|
8889
|
+
const { text, suggestions } = parseSuggestions(block.text);
|
|
8890
|
+
if (suggestions.length === 0) {
|
|
8891
|
+
continue;
|
|
8892
|
+
}
|
|
8893
|
+
block.displayText = text;
|
|
8894
|
+
block.suggestions = suggestions;
|
|
8895
|
+
}
|
|
8896
|
+
}
|
|
8897
|
+
var SUGGEST_MARKER, SEPARATORS, SEPARATORS_ONLY, TRAILING_SEPARATORS, LIST_MARKER, FENCE;
|
|
8898
|
+
var init_suggestions = __esm({
|
|
8899
|
+
"src/suggestions.ts"() {
|
|
8900
|
+
"use strict";
|
|
8901
|
+
SUGGEST_MARKER = "](suggest:";
|
|
8902
|
+
SEPARATORS = "\\s\xB7|,\\-\u2013\u2014";
|
|
8903
|
+
SEPARATORS_ONLY = new RegExp(`^[${SEPARATORS}]*$`);
|
|
8904
|
+
TRAILING_SEPARATORS = new RegExp(`[${SEPARATORS}]+$`);
|
|
8905
|
+
LIST_MARKER = /^\s*(?:[-*+]|\d+[.)])\s+/;
|
|
8906
|
+
FENCE = /^ {0,3}(?:```|~~~)/;
|
|
8907
|
+
}
|
|
8908
|
+
});
|
|
8909
|
+
|
|
8755
8910
|
// src/brandExtraction/index.ts
|
|
8756
8911
|
import fs20 from "fs";
|
|
8757
8912
|
import path11 from "path";
|
|
@@ -9285,8 +9440,27 @@ async function runTurn(params) {
|
|
|
9285
9440
|
toolInputAccumulators.set(id, acc);
|
|
9286
9441
|
}
|
|
9287
9442
|
return acc;
|
|
9443
|
+
}, emitTextBlockSnapshot2 = function(closing) {
|
|
9444
|
+
let block;
|
|
9445
|
+
for (let i = contentBlocks.length - 1; i >= 0; i--) {
|
|
9446
|
+
const candidate = contentBlocks[i];
|
|
9447
|
+
if (candidate.type === "text") {
|
|
9448
|
+
block = candidate;
|
|
9449
|
+
break;
|
|
9450
|
+
}
|
|
9451
|
+
}
|
|
9452
|
+
if (!block || !block.text.includes(SUGGEST_MARKER)) {
|
|
9453
|
+
return;
|
|
9454
|
+
}
|
|
9455
|
+
const { text, suggestions } = parseSuggestions(block.text);
|
|
9456
|
+
const worthSending = closing ? suggestions.length > 0 : suggestions.length > emittedSuggestionCount;
|
|
9457
|
+
if (!worthSending) {
|
|
9458
|
+
return;
|
|
9459
|
+
}
|
|
9460
|
+
emittedSuggestionCount = suggestions.length;
|
|
9461
|
+
onEvent({ type: "text_block", text, suggestions });
|
|
9288
9462
|
};
|
|
9289
|
-
var getOrCreateAccumulator = getOrCreateAccumulator2;
|
|
9463
|
+
var getOrCreateAccumulator = getOrCreateAccumulator2, emitTextBlockSnapshot = emitTextBlockSnapshot2;
|
|
9290
9464
|
if (signal?.aborted) {
|
|
9291
9465
|
onEvent({ type: "turn_cancelled" });
|
|
9292
9466
|
saveSession(state);
|
|
@@ -9298,6 +9472,7 @@ async function runTurn(params) {
|
|
|
9298
9472
|
let thinkingCompleteCount = 0;
|
|
9299
9473
|
let lastThinkingRelatedStartedAt;
|
|
9300
9474
|
let textBlockOpen = false;
|
|
9475
|
+
let emittedSuggestionCount = 0;
|
|
9301
9476
|
const toolInputAccumulators = /* @__PURE__ */ new Map();
|
|
9302
9477
|
let stopReason = "end_turn";
|
|
9303
9478
|
let turnProviderMetadata;
|
|
@@ -9386,14 +9561,19 @@ async function runTurn(params) {
|
|
|
9386
9561
|
text: event.text,
|
|
9387
9562
|
startedAt: event.ts
|
|
9388
9563
|
});
|
|
9564
|
+
emittedSuggestionCount = 0;
|
|
9389
9565
|
}
|
|
9390
9566
|
textBlockOpen = true;
|
|
9391
9567
|
onEvent({ type: "text", text: event.text });
|
|
9568
|
+
emitTextBlockSnapshot2(false);
|
|
9392
9569
|
break;
|
|
9393
9570
|
}
|
|
9394
9571
|
case "thinking":
|
|
9395
9572
|
if (event.text === "") {
|
|
9396
9573
|
thinkingBlockStartTimes.push(event.ts);
|
|
9574
|
+
if (textBlockOpen) {
|
|
9575
|
+
emitTextBlockSnapshot2(true);
|
|
9576
|
+
}
|
|
9397
9577
|
textBlockOpen = false;
|
|
9398
9578
|
}
|
|
9399
9579
|
onEvent({ type: "thinking", text: event.text });
|
|
@@ -9519,6 +9699,11 @@ async function runTurn(params) {
|
|
|
9519
9699
|
throw err;
|
|
9520
9700
|
}
|
|
9521
9701
|
}
|
|
9702
|
+
if (textBlockOpen) {
|
|
9703
|
+
emitTextBlockSnapshot2(true);
|
|
9704
|
+
textBlockOpen = false;
|
|
9705
|
+
}
|
|
9706
|
+
annotateSuggestions(contentBlocks);
|
|
9522
9707
|
if (signal?.aborted) {
|
|
9523
9708
|
statusWatcher.stop();
|
|
9524
9709
|
if (contentBlocks.length > 0) {
|
|
@@ -9802,6 +9987,7 @@ var init_agent = __esm({
|
|
|
9802
9987
|
init_resolve();
|
|
9803
9988
|
init_errors();
|
|
9804
9989
|
init_cleanMessages();
|
|
9990
|
+
init_suggestions();
|
|
9805
9991
|
init_sentinel();
|
|
9806
9992
|
init_trigger2();
|
|
9807
9993
|
init_surfaces();
|
|
@@ -10992,6 +11178,13 @@ var init_headless = __esm({
|
|
|
10992
11178
|
rid
|
|
10993
11179
|
);
|
|
10994
11180
|
return;
|
|
11181
|
+
case "text_block":
|
|
11182
|
+
this.emit(
|
|
11183
|
+
"text_block",
|
|
11184
|
+
{ text: e.text, suggestions: e.suggestions },
|
|
11185
|
+
rid
|
|
11186
|
+
);
|
|
11187
|
+
return;
|
|
10995
11188
|
case "thinking":
|
|
10996
11189
|
this.emit(
|
|
10997
11190
|
"thinking",
|
|
@@ -14,7 +14,7 @@ Guidance for designing conversational AI agents and their frontends. An agent in
|
|
|
14
14
|
|
|
15
15
|
A good system prompt establishes who the agent is — personality, tone, judgment style, the kind of person they sound like. It doesn't enumerate every possible interaction or restate what tools already describe.
|
|
16
16
|
|
|
17
|
-
Short and opinionated beats long and comprehensive. "Sounds like a sharp, organized friend — brief by default" gives the model more to work with than a page of behavioral rules. Define constraints through character, not checklists. Let the model's judgment work.
|
|
17
|
+
Short and opinionated beats long and comprehensive. "Sounds like a sharp, organized friend — brief by default" gives the model more to work with than a page of behavioral rules. Define constraints through character, not checklists. Let the model's judgment work. Start minimal and add rules only for behaviors that actually misfire once the user has tested it — the thread log is the feedback loop, and `remy-admin agent threads get` reads a conversation verbatim (see "The agent CLI" below).
|
|
18
18
|
|
|
19
19
|
Three things every compiled system prompt should carry, on top of the character:
|
|
20
20
|
|
|
@@ -213,7 +213,7 @@ When the user sends a message, add it to the conversation immediately — don't
|
|
|
213
213
|
|
|
214
214
|
### Tool calls
|
|
215
215
|
|
|
216
|
-
Show tool activity in the chat as a compact, inline status that appears when `onToolCallStart` fires and resolves when `onToolCallResult` arrives. Never show raw JSON, tool IDs, or internal details — just a human-readable description of what's happening.
|
|
216
|
+
Show tool activity in the chat as a compact, inline status that appears when `onToolCallStart` fires and resolves when `onToolCallResult` arrives. Never show raw JSON, tool IDs, or internal details — just a human-readable description of what's happening. Tool calls should be interleaved into the conversation so they flow naturally as part of the agent's response.
|
|
217
217
|
|
|
218
218
|
### Input area
|
|
219
219
|
|
|
@@ -236,6 +236,21 @@ The chat UI uses the app's design system — colors, typography, voice from `@br
|
|
|
236
236
|
- Avoid designs that look like dated messaging apps from 2015
|
|
237
237
|
- Avoid robotic empty states ("Hello! I'm your AI assistant. How can I help you today?")
|
|
238
238
|
|
|
239
|
+
## The agent CLI
|
|
240
|
+
|
|
241
|
+
The `remy-admin agent` family is the conversation log — every thread the deployed agent has had, and each one's full transcript:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
remy-admin agent threads list --limit 10 # newest activity first
|
|
245
|
+
remy-admin agent threads get <threadId> # full transcript: messages + tool calls
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Transcripts are how you iterate on an agent: after the user tests it, read `agent threads get` for what was actually said and which tools ran with which arguments — a tool the agent never reached for, one it called with the wrong shape, a reply that ignored the result — and fix the system prompt and tool descriptions from that evidence rather than guesses.
|
|
249
|
+
|
|
250
|
+
In the list, `toolErrorCount` and `hasTurnError` point at the conversations worth opening (a failed tool call; a turn that broke on a model error, rate limit, or credits instead of replying). `devSession` is true for your own test conversations through the dev tunnel, so you can tell them from real traffic.
|
|
251
|
+
|
|
252
|
+
In a transcript, each message keeps the stored conversation's own shape: `user` for a person's message and also for a tool result (which carries `toolCallId`), `assistant` for the agent (carrying `toolCalls` when it asked for tools). Every method tool call also carries a `requestId` — `remy-admin requests get <requestId>` opens that call's input, output, `console.log` output and error, which is how you get from "the agent said something wrong" to the method that gave it bad data. Client tools run in the browser, so they have no requestId.
|
|
253
|
+
|
|
239
254
|
---
|
|
240
255
|
|
|
241
256
|
# The wiring
|
|
@@ -317,6 +317,8 @@ export async function regenerateCache(input: {}) {
|
|
|
317
317
|
|
|
318
318
|
Web frontend calls (`/_/methods`), API interface calls (`/_/api`), and agent chat all run as the authenticated user — they don't get the system role unless the user has been explicitly assigned it. You can assign `system` to app users via the dashboard or SDK if they need to manually trigger these methods.
|
|
319
319
|
|
|
320
|
+
The system role is a platform-minted identity, not a row in the app's users table, so it does not require auth. A cron-only app with no login and no user table can gate a method on `requireRole('system')` and it works. To exercise one in dev, run it with `roles: ["system"]` — `runMethod` gives it the same identity a real platform trigger gets, so the gate stays in place while you test.
|
|
321
|
+
|
|
320
322
|
## Login Page Example
|
|
321
323
|
|
|
322
324
|
```tsx
|
|
@@ -461,7 +463,7 @@ Auth works the same in dev/preview as in production — real verification codes
|
|
|
461
463
|
|
|
462
464
|
All other emails and phone numbers receive real codes. There is no dev-mode bypass, no fake code, and no way to skip verification. When testing auth flows in the preview, use one of the test bypasses above or a real email/phone. (These dev bypasses work in dev sessions only and exist for you — they're distinct from *test accounts*, the platform setting for giving external reviewers a fixed-code login that works in production; see *Restricting Who Can Sign Up*.)
|
|
463
465
|
|
|
464
|
-
This test account is the dev's standing identity: the preview's sign-in helper auto-fills it, the editor's Roles column edits its roles, and a scenario's `roles` field assigns roles to it after seeding. The `runMethod` tool's `userId: "testUser"` shortcut resolves to this same dev-bypass identity
|
|
466
|
+
This test account is the dev's standing identity: the preview's sign-in helper auto-fills it, the editor's Roles column edits its roles, and a scenario's `roles` field assigns roles to it after seeding. The `runMethod` tool's `userId: "testUser"` shortcut resolves to this same dev-bypass identity, as does `roles` without a `userId` (except `roles: ["system"]`, which is platform-minted and needs no user row — see *System Role*). The platform find-or-creates a real users-table row for it on first call and caches the row's UUID for the rest of the dev session. **`auth.userId` inside the method is that UUID — not the literal string `"testUser"`.** The user row already exists, so don't try to insert it. If you need the UUID to seed app-specific rows that reference it (profiles, preferences, foreign keys), read it from any method response or query the users table directly: `SELECT id FROM users WHERE email = 'remy@mindstudio.ai'` (or `phone = '+15555555555'` for SMS-auth apps).
|
|
465
467
|
|
|
466
468
|
For **"Sign in with Remy"** apps (`auth.methods` is `["remy"]`, with no `email-code`/`sms-code`), `testUser` — and `setupBrowser`, and the editor's Roles column — resolve to **the developer's own delegated Remy identity**, not the `remy@mindstudio.ai` code-bypass user. `auth.userId` is still that user's real UUID, but the `remy@mindstudio.ai` email lookup above does not apply — read the UUID from a method response instead.
|
|
467
469
|
|
|
@@ -38,7 +38,7 @@ You will occasionally receive automated messages prefixed with `@@automated_mess
|
|
|
38
38
|
|
|
39
39
|
## Style
|
|
40
40
|
- Your messages are rendered as markdown. Use formatting (headers, bold, lists, code blocks) when it helps readability. You can include images using `` — use this to show the user screenshots, generated images, or other visual references inline in your messages.
|
|
41
|
-
- When offering suggestions or options the user might want to quickly select in a conversation, format them as clickable suggestion links: `[label](suggest:message sent on click)`. The label renders as a tappable chip and should be a few words — chip-sized, not sentence-sized. The `suggest:` payload can be longer; that's what gets sent as the user's next message when clicked. Use these liberally: when brainstorming, offering directions, listing options, or any time you're asking a question the user could answer with a quick tap. When explicitly gathering information from the user, however, always use the `promptUser` tool instead.
|
|
41
|
+
- When offering suggestions or options the user might want to quickly select in a conversation, format them as clickable suggestion links: `[label](suggest:message sent on click)`. The label renders as a tappable chip and should be a few words — chip-sized, not sentence-sized. The `suggest:` payload can be longer; that's what gets sent as the user's next message when clicked. A run of links on its own line, or at the end of a line, is lifted out of your message and rendered as chips beneath it; a link written mid-sentence keeps its label in the sentence and offers the chip as well. Use these liberally: when brainstorming, offering directions, listing options, or any time you're asking a question the user could answer with a quick tap. When explicitly gathering information from the user, however, always use the `promptUser` tool instead.
|
|
42
42
|
- When pointing the user to a specific page in their running app, format the link as `[label](preview:/path)` — clicking it navigates the live preview there. The payload is a path-relative URL (just `/...`, with optional query/hash); for external URLs, use a plain markdown link.
|
|
43
43
|
- Keep language accessible. Describe what the app *does*, not how it's implemented, unless the user demonstrates technical fluency.
|
|
44
44
|
- Always use full paths relative to the project root when mentioning files (`dist/interfaces/web/src/App.tsx`, not `App.tsx`). Paths will be rendered as clickable links for the user.
|