@mindstudio-ai/remy 0.1.316 → 0.1.317
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.d.ts +16 -0
- package/dist/headless.js +100 -7
- package/dist/index.js +100 -7
- package/dist/prompt/compiled/tables.md +2 -0
- package/package.json +1 -1
package/dist/headless.d.ts
CHANGED
|
@@ -137,6 +137,7 @@ declare class HeadlessSession {
|
|
|
137
137
|
*/
|
|
138
138
|
private runForcedCompactionIfNeeded;
|
|
139
139
|
private onBackgroundComplete;
|
|
140
|
+
private awaitExternalToolResult;
|
|
140
141
|
private resolveExternalTool;
|
|
141
142
|
private onEvent;
|
|
142
143
|
/**
|
|
@@ -149,6 +150,21 @@ declare class HeadlessSession {
|
|
|
149
150
|
* filename de-dup set is per-call, so parallel calls race on names.
|
|
150
151
|
*/
|
|
151
152
|
private persistEntryAttachments;
|
|
153
|
+
/**
|
|
154
|
+
* promptUser `file` answers arrive as private-upload descriptors
|
|
155
|
+
* ({url, key, filename, extractedTextUrl}) — the same shape as a chat
|
|
156
|
+
* attachment. Download them to src/.user-uploads/ and rewrite each answer to
|
|
157
|
+
* the local path(s) BEFORE the result settles, so the model, the tool block,
|
|
158
|
+
* the saved session and the frontend transcript all see paths and never a
|
|
159
|
+
* signed url that dies in an hour. Non-file answers, dismissals and anything
|
|
160
|
+
* unparseable pass through untouched.
|
|
161
|
+
*
|
|
162
|
+
* Runs while the turn is blocked on this tool, so it can't overlap the
|
|
163
|
+
* turn's own persistEntryAttachments; the ASAP/queue persist paths only run
|
|
164
|
+
* after the tool batch settles. One prompt is open at a time, so two results
|
|
165
|
+
* can't race either.
|
|
166
|
+
*/
|
|
167
|
+
private persistPromptUserUploads;
|
|
152
168
|
/**
|
|
153
169
|
* Stable id for a queued chain step, so the frontend can address one (to
|
|
154
170
|
* discard a paused pipeline) rather than waiting for drainQueueLoop to
|
package/dist/headless.js
CHANGED
|
@@ -1590,7 +1590,7 @@ var promptUserTool = {
|
|
|
1590
1590
|
type: {
|
|
1591
1591
|
type: "string",
|
|
1592
1592
|
enum: ["select", "checklist", "text", "file"],
|
|
1593
|
-
description: 'select: pick one from a list. checklist: pick one or more from a list. The user can always provide a custom "Other" answer for select and checklist questions, so there is no need to include an "Other" option. text: free-form input. file: file/image upload
|
|
1593
|
+
description: 'select: pick one from a list. checklist: pick one or more from a list. The user can always provide a custom "Other" answer for select and checklist questions, so there is no need to include an "Other" option. text: free-form input. file: file/image upload. The answer is the local path (string) under src/.user-uploads/ \u2014 the file is already downloaded to disk. Documents may have an extracted-text sidecar at <path>.txt. Reference the path directly; pass image paths straight to analyzeImage / screenshot tools.'
|
|
1594
1594
|
},
|
|
1595
1595
|
helpText: {
|
|
1596
1596
|
type: "string",
|
|
@@ -1625,7 +1625,7 @@ var promptUserTool = {
|
|
|
1625
1625
|
},
|
|
1626
1626
|
multiple: {
|
|
1627
1627
|
type: "boolean",
|
|
1628
|
-
description: "For file type: allow multiple uploads (
|
|
1628
|
+
description: "For file type: allow multiple uploads (the answer is an array of local paths). Defaults to false."
|
|
1629
1629
|
},
|
|
1630
1630
|
format: {
|
|
1631
1631
|
type: "string",
|
|
@@ -9601,10 +9601,10 @@ function isImageAttachment(att) {
|
|
|
9601
9601
|
const name = att.filename || filenameFromUrl(att.url);
|
|
9602
9602
|
return IMAGE_EXTENSIONS.has(extname3(name).toLowerCase());
|
|
9603
9603
|
}
|
|
9604
|
-
async function
|
|
9604
|
+
async function persistAttachmentList(attachments) {
|
|
9605
9605
|
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
9606
9606
|
if (nonVoice.length === 0) {
|
|
9607
|
-
return
|
|
9607
|
+
return [];
|
|
9608
9608
|
}
|
|
9609
9609
|
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
9610
9610
|
const claimed = /* @__PURE__ */ new Set();
|
|
@@ -9655,8 +9655,13 @@ async function persistAttachments(attachments) {
|
|
|
9655
9655
|
};
|
|
9656
9656
|
})
|
|
9657
9657
|
);
|
|
9658
|
-
|
|
9659
|
-
|
|
9658
|
+
return results.map((r) => r.status === "fulfilled" ? r.value : null);
|
|
9659
|
+
}
|
|
9660
|
+
async function persistAttachments(attachments) {
|
|
9661
|
+
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
9662
|
+
const results = await persistAttachmentList(nonVoice);
|
|
9663
|
+
const settled = results.map((result, i) => ({
|
|
9664
|
+
result,
|
|
9660
9665
|
isImage: isImageAttachment(nonVoice[i])
|
|
9661
9666
|
}));
|
|
9662
9667
|
return {
|
|
@@ -10345,7 +10350,7 @@ var HeadlessSession = class {
|
|
|
10345
10350
|
//////////////////////////////////////////////////////////////////////////////
|
|
10346
10351
|
// External tool bridge
|
|
10347
10352
|
//////////////////////////////////////////////////////////////////////////////
|
|
10348
|
-
|
|
10353
|
+
awaitExternalToolResult = (id, name) => {
|
|
10349
10354
|
const early = this.earlyResults.get(id);
|
|
10350
10355
|
if (early !== void 0) {
|
|
10351
10356
|
this.earlyResults.delete(id);
|
|
@@ -10369,6 +10374,13 @@ var HeadlessSession = class {
|
|
|
10369
10374
|
});
|
|
10370
10375
|
});
|
|
10371
10376
|
};
|
|
10377
|
+
// promptUser answers carry private-upload descriptors for `file` questions;
|
|
10378
|
+
// persist them to disk and hand the model local paths (see
|
|
10379
|
+
// persistPromptUserUploads). Every other external tool passes through.
|
|
10380
|
+
resolveExternalTool = (id, name, input) => {
|
|
10381
|
+
const result = this.awaitExternalToolResult(id, name);
|
|
10382
|
+
return name === "promptUser" ? result.then((raw) => this.persistPromptUserUploads(input, raw)) : result;
|
|
10383
|
+
};
|
|
10372
10384
|
//////////////////////////////////////////////////////////////////////////////
|
|
10373
10385
|
// AgentEvent → wire protocol translation
|
|
10374
10386
|
//////////////////////////////////////////////////////////////////////////////
|
|
@@ -10574,6 +10586,87 @@ var HeadlessSession = class {
|
|
|
10574
10586
|
return void 0;
|
|
10575
10587
|
}
|
|
10576
10588
|
}
|
|
10589
|
+
/**
|
|
10590
|
+
* promptUser `file` answers arrive as private-upload descriptors
|
|
10591
|
+
* ({url, key, filename, extractedTextUrl}) — the same shape as a chat
|
|
10592
|
+
* attachment. Download them to src/.user-uploads/ and rewrite each answer to
|
|
10593
|
+
* the local path(s) BEFORE the result settles, so the model, the tool block,
|
|
10594
|
+
* the saved session and the frontend transcript all see paths and never a
|
|
10595
|
+
* signed url that dies in an hour. Non-file answers, dismissals and anything
|
|
10596
|
+
* unparseable pass through untouched.
|
|
10597
|
+
*
|
|
10598
|
+
* Runs while the turn is blocked on this tool, so it can't overlap the
|
|
10599
|
+
* turn's own persistEntryAttachments; the ASAP/queue persist paths only run
|
|
10600
|
+
* after the tool batch settles. One prompt is open at a time, so two results
|
|
10601
|
+
* can't race either.
|
|
10602
|
+
*/
|
|
10603
|
+
async persistPromptUserUploads(input, raw) {
|
|
10604
|
+
let answers;
|
|
10605
|
+
try {
|
|
10606
|
+
answers = JSON.parse(raw);
|
|
10607
|
+
} catch {
|
|
10608
|
+
return raw;
|
|
10609
|
+
}
|
|
10610
|
+
if (!answers || typeof answers !== "object" || Array.isArray(answers) || answers._dismissed) {
|
|
10611
|
+
return raw;
|
|
10612
|
+
}
|
|
10613
|
+
const isDescriptor = (d) => !!d && typeof d === "object" && typeof d.url === "string";
|
|
10614
|
+
const questions = Array.isArray(input?.questions) ? input.questions : [];
|
|
10615
|
+
const batch = [];
|
|
10616
|
+
const slots = [];
|
|
10617
|
+
for (const q of questions) {
|
|
10618
|
+
if (q?.type !== "file" || typeof q.id !== "string") {
|
|
10619
|
+
continue;
|
|
10620
|
+
}
|
|
10621
|
+
const val = answers[q.id];
|
|
10622
|
+
const items = (Array.isArray(val) ? val : [val]).filter(isDescriptor);
|
|
10623
|
+
if (items.length === 0) {
|
|
10624
|
+
continue;
|
|
10625
|
+
}
|
|
10626
|
+
slots.push({
|
|
10627
|
+
id: q.id,
|
|
10628
|
+
isArray: Array.isArray(val),
|
|
10629
|
+
count: items.length
|
|
10630
|
+
});
|
|
10631
|
+
for (const d of items) {
|
|
10632
|
+
batch.push({
|
|
10633
|
+
url: d.url,
|
|
10634
|
+
key: d.key,
|
|
10635
|
+
filename: d.filename,
|
|
10636
|
+
extractedTextUrl: d.extractedTextUrl ?? void 0
|
|
10637
|
+
});
|
|
10638
|
+
}
|
|
10639
|
+
}
|
|
10640
|
+
if (batch.length === 0) {
|
|
10641
|
+
return raw;
|
|
10642
|
+
}
|
|
10643
|
+
let results;
|
|
10644
|
+
try {
|
|
10645
|
+
results = await persistAttachmentList(batch);
|
|
10646
|
+
} catch (err) {
|
|
10647
|
+
log17.warn("promptUser upload persistence failed", {
|
|
10648
|
+
error: err.message
|
|
10649
|
+
});
|
|
10650
|
+
results = batch.map(() => null);
|
|
10651
|
+
}
|
|
10652
|
+
let cursor = 0;
|
|
10653
|
+
for (const slot of slots) {
|
|
10654
|
+
const paths = results.slice(cursor, cursor + slot.count).map((r, i) => {
|
|
10655
|
+
if (r) {
|
|
10656
|
+
return r.localPath;
|
|
10657
|
+
}
|
|
10658
|
+
const att = batch[cursor + i];
|
|
10659
|
+
log17.warn("promptUser upload not persisted; falling back to url", {
|
|
10660
|
+
filename: att.filename
|
|
10661
|
+
});
|
|
10662
|
+
return att.url;
|
|
10663
|
+
});
|
|
10664
|
+
cursor += slot.count;
|
|
10665
|
+
answers[slot.id] = slot.isArray ? paths : paths[0];
|
|
10666
|
+
}
|
|
10667
|
+
log17.info("promptUser uploads persisted", { count: batch.length });
|
|
10668
|
+
return JSON.stringify(answers);
|
|
10669
|
+
}
|
|
10577
10670
|
/**
|
|
10578
10671
|
* Stable id for a queued chain step, so the frontend can address one (to
|
|
10579
10672
|
* discard a paused pipeline) rather than waiting for drainQueueLoop to
|
package/dist/index.js
CHANGED
|
@@ -1064,7 +1064,7 @@ var init_promptUser = __esm({
|
|
|
1064
1064
|
type: {
|
|
1065
1065
|
type: "string",
|
|
1066
1066
|
enum: ["select", "checklist", "text", "file"],
|
|
1067
|
-
description: 'select: pick one from a list. checklist: pick one or more from a list. The user can always provide a custom "Other" answer for select and checklist questions, so there is no need to include an "Other" option. text: free-form input. file: file/image upload
|
|
1067
|
+
description: 'select: pick one from a list. checklist: pick one or more from a list. The user can always provide a custom "Other" answer for select and checklist questions, so there is no need to include an "Other" option. text: free-form input. file: file/image upload. The answer is the local path (string) under src/.user-uploads/ \u2014 the file is already downloaded to disk. Documents may have an extracted-text sidecar at <path>.txt. Reference the path directly; pass image paths straight to analyzeImage / screenshot tools.'
|
|
1068
1068
|
},
|
|
1069
1069
|
helpText: {
|
|
1070
1070
|
type: "string",
|
|
@@ -1099,7 +1099,7 @@ var init_promptUser = __esm({
|
|
|
1099
1099
|
},
|
|
1100
1100
|
multiple: {
|
|
1101
1101
|
type: "boolean",
|
|
1102
|
-
description: "For file type: allow multiple uploads (
|
|
1102
|
+
description: "For file type: allow multiple uploads (the answer is an array of local paths). Defaults to false."
|
|
1103
1103
|
},
|
|
1104
1104
|
format: {
|
|
1105
1105
|
type: "string",
|
|
@@ -10552,10 +10552,10 @@ function isImageAttachment(att) {
|
|
|
10552
10552
|
const name = att.filename || filenameFromUrl(att.url);
|
|
10553
10553
|
return IMAGE_EXTENSIONS.has(extname3(name).toLowerCase());
|
|
10554
10554
|
}
|
|
10555
|
-
async function
|
|
10555
|
+
async function persistAttachmentList(attachments) {
|
|
10556
10556
|
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
10557
10557
|
if (nonVoice.length === 0) {
|
|
10558
|
-
return
|
|
10558
|
+
return [];
|
|
10559
10559
|
}
|
|
10560
10560
|
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
10561
10561
|
const claimed = /* @__PURE__ */ new Set();
|
|
@@ -10606,8 +10606,13 @@ async function persistAttachments(attachments) {
|
|
|
10606
10606
|
};
|
|
10607
10607
|
})
|
|
10608
10608
|
);
|
|
10609
|
-
|
|
10610
|
-
|
|
10609
|
+
return results.map((r) => r.status === "fulfilled" ? r.value : null);
|
|
10610
|
+
}
|
|
10611
|
+
async function persistAttachments(attachments) {
|
|
10612
|
+
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
10613
|
+
const results = await persistAttachmentList(nonVoice);
|
|
10614
|
+
const settled = results.map((result, i) => ({
|
|
10615
|
+
result,
|
|
10611
10616
|
isImage: isImageAttachment(nonVoice[i])
|
|
10612
10617
|
}));
|
|
10613
10618
|
return {
|
|
@@ -11353,7 +11358,7 @@ var init_headless = __esm({
|
|
|
11353
11358
|
//////////////////////////////////////////////////////////////////////////////
|
|
11354
11359
|
// External tool bridge
|
|
11355
11360
|
//////////////////////////////////////////////////////////////////////////////
|
|
11356
|
-
|
|
11361
|
+
awaitExternalToolResult = (id, name) => {
|
|
11357
11362
|
const early = this.earlyResults.get(id);
|
|
11358
11363
|
if (early !== void 0) {
|
|
11359
11364
|
this.earlyResults.delete(id);
|
|
@@ -11377,6 +11382,13 @@ var init_headless = __esm({
|
|
|
11377
11382
|
});
|
|
11378
11383
|
});
|
|
11379
11384
|
};
|
|
11385
|
+
// promptUser answers carry private-upload descriptors for `file` questions;
|
|
11386
|
+
// persist them to disk and hand the model local paths (see
|
|
11387
|
+
// persistPromptUserUploads). Every other external tool passes through.
|
|
11388
|
+
resolveExternalTool = (id, name, input) => {
|
|
11389
|
+
const result = this.awaitExternalToolResult(id, name);
|
|
11390
|
+
return name === "promptUser" ? result.then((raw) => this.persistPromptUserUploads(input, raw)) : result;
|
|
11391
|
+
};
|
|
11380
11392
|
//////////////////////////////////////////////////////////////////////////////
|
|
11381
11393
|
// AgentEvent → wire protocol translation
|
|
11382
11394
|
//////////////////////////////////////////////////////////////////////////////
|
|
@@ -11582,6 +11594,87 @@ var init_headless = __esm({
|
|
|
11582
11594
|
return void 0;
|
|
11583
11595
|
}
|
|
11584
11596
|
}
|
|
11597
|
+
/**
|
|
11598
|
+
* promptUser `file` answers arrive as private-upload descriptors
|
|
11599
|
+
* ({url, key, filename, extractedTextUrl}) — the same shape as a chat
|
|
11600
|
+
* attachment. Download them to src/.user-uploads/ and rewrite each answer to
|
|
11601
|
+
* the local path(s) BEFORE the result settles, so the model, the tool block,
|
|
11602
|
+
* the saved session and the frontend transcript all see paths and never a
|
|
11603
|
+
* signed url that dies in an hour. Non-file answers, dismissals and anything
|
|
11604
|
+
* unparseable pass through untouched.
|
|
11605
|
+
*
|
|
11606
|
+
* Runs while the turn is blocked on this tool, so it can't overlap the
|
|
11607
|
+
* turn's own persistEntryAttachments; the ASAP/queue persist paths only run
|
|
11608
|
+
* after the tool batch settles. One prompt is open at a time, so two results
|
|
11609
|
+
* can't race either.
|
|
11610
|
+
*/
|
|
11611
|
+
async persistPromptUserUploads(input, raw) {
|
|
11612
|
+
let answers;
|
|
11613
|
+
try {
|
|
11614
|
+
answers = JSON.parse(raw);
|
|
11615
|
+
} catch {
|
|
11616
|
+
return raw;
|
|
11617
|
+
}
|
|
11618
|
+
if (!answers || typeof answers !== "object" || Array.isArray(answers) || answers._dismissed) {
|
|
11619
|
+
return raw;
|
|
11620
|
+
}
|
|
11621
|
+
const isDescriptor = (d) => !!d && typeof d === "object" && typeof d.url === "string";
|
|
11622
|
+
const questions = Array.isArray(input?.questions) ? input.questions : [];
|
|
11623
|
+
const batch = [];
|
|
11624
|
+
const slots = [];
|
|
11625
|
+
for (const q of questions) {
|
|
11626
|
+
if (q?.type !== "file" || typeof q.id !== "string") {
|
|
11627
|
+
continue;
|
|
11628
|
+
}
|
|
11629
|
+
const val = answers[q.id];
|
|
11630
|
+
const items = (Array.isArray(val) ? val : [val]).filter(isDescriptor);
|
|
11631
|
+
if (items.length === 0) {
|
|
11632
|
+
continue;
|
|
11633
|
+
}
|
|
11634
|
+
slots.push({
|
|
11635
|
+
id: q.id,
|
|
11636
|
+
isArray: Array.isArray(val),
|
|
11637
|
+
count: items.length
|
|
11638
|
+
});
|
|
11639
|
+
for (const d of items) {
|
|
11640
|
+
batch.push({
|
|
11641
|
+
url: d.url,
|
|
11642
|
+
key: d.key,
|
|
11643
|
+
filename: d.filename,
|
|
11644
|
+
extractedTextUrl: d.extractedTextUrl ?? void 0
|
|
11645
|
+
});
|
|
11646
|
+
}
|
|
11647
|
+
}
|
|
11648
|
+
if (batch.length === 0) {
|
|
11649
|
+
return raw;
|
|
11650
|
+
}
|
|
11651
|
+
let results;
|
|
11652
|
+
try {
|
|
11653
|
+
results = await persistAttachmentList(batch);
|
|
11654
|
+
} catch (err) {
|
|
11655
|
+
log17.warn("promptUser upload persistence failed", {
|
|
11656
|
+
error: err.message
|
|
11657
|
+
});
|
|
11658
|
+
results = batch.map(() => null);
|
|
11659
|
+
}
|
|
11660
|
+
let cursor = 0;
|
|
11661
|
+
for (const slot of slots) {
|
|
11662
|
+
const paths = results.slice(cursor, cursor + slot.count).map((r, i) => {
|
|
11663
|
+
if (r) {
|
|
11664
|
+
return r.localPath;
|
|
11665
|
+
}
|
|
11666
|
+
const att = batch[cursor + i];
|
|
11667
|
+
log17.warn("promptUser upload not persisted; falling back to url", {
|
|
11668
|
+
filename: att.filename
|
|
11669
|
+
});
|
|
11670
|
+
return att.url;
|
|
11671
|
+
});
|
|
11672
|
+
cursor += slot.count;
|
|
11673
|
+
answers[slot.id] = slot.isArray ? paths : paths[0];
|
|
11674
|
+
}
|
|
11675
|
+
log17.info("promptUser uploads persisted", { count: batch.length });
|
|
11676
|
+
return JSON.stringify(answers);
|
|
11677
|
+
}
|
|
11585
11678
|
/**
|
|
11586
11679
|
* Stable id for a queued chain step, so the frontend can address one (to
|
|
11587
11680
|
* discard a paused pipeline) rather than waiting for drainQueueLoop to
|
|
@@ -216,6 +216,8 @@ const rows = await db.sql<{ questionId: string; n: number }>(
|
|
|
216
216
|
|
|
217
217
|
`SELECT`/`WITH` only — writes throw; use Table methods for writes. Positional `?` bind params. Lazy and batchable via `db.batch()`. Raw rows come back close to how SQLite stores them and may not exactly match the typed API's representations.
|
|
218
218
|
|
|
219
|
+
Every read, typed or raw, returns at most 100,000 rows or 64MB of JSON; past either limit the query fails with `result_too_large`. Paginate, select fewer columns, or use an aggregate instead.
|
|
220
|
+
|
|
219
221
|
### Updating Records
|
|
220
222
|
|
|
221
223
|
```typescript
|