@mindstudio-ai/remy 0.1.148 → 0.1.149
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/automatedActions/approveInitialPlan.md +14 -0
- package/dist/automatedActions/buildFromInitialSpec.md +5 -3
- package/dist/automatedActions/buildFromRoadmap.md +4 -0
- package/dist/automatedActions/postBuildPolish.md +2 -2
- package/dist/headless.d.ts +91 -2
- package/dist/headless.js +746 -486
- package/dist/index.js +1067 -768
- package/dist/prompt/compiled/design.md +1 -0
- package/dist/prompt/static/authoring.md +0 -20
- package/dist/prompt/static/coding.md +1 -0
- package/dist/prompt/static/instructions.md +1 -1
- package/dist/prompt/static/intake.md +4 -6
- package/dist/prompt/static/team.md +3 -1
- package/dist/subagents/browserAutomation/prompt.md +1 -0
- package/dist/subagents/productVision/pitch-deck-shell.html +37 -7
- package/package.json +1 -1
package/dist/headless.js
CHANGED
|
@@ -4,17 +4,8 @@ var __export = (target, all) => {
|
|
|
4
4
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
// src/headless.ts
|
|
7
|
+
// src/headless/index.ts
|
|
8
8
|
import { createInterface } from "readline";
|
|
9
|
-
import {
|
|
10
|
-
writeFileSync,
|
|
11
|
-
readFileSync,
|
|
12
|
-
unlinkSync,
|
|
13
|
-
mkdirSync,
|
|
14
|
-
existsSync
|
|
15
|
-
} from "fs";
|
|
16
|
-
import { writeFile } from "fs/promises";
|
|
17
|
-
import { basename, join, extname } from "path";
|
|
18
9
|
|
|
19
10
|
// src/logger.ts
|
|
20
11
|
import fs from "fs";
|
|
@@ -377,11 +368,11 @@ Old tool results are periodically cleared from the conversation to save context
|
|
|
377
368
|
</conversation_summaries>
|
|
378
369
|
|
|
379
370
|
<project_onboarding>
|
|
380
|
-
New projects progress through
|
|
371
|
+
New projects progress through three onboarding states. The user might skip this entirely and jump straight into working on the existing scaffold (which defaults to onboardingFinished), but ideally new projects move through each phase:
|
|
381
372
|
|
|
382
|
-
- **intake**: Gathering requirements. The project has scaffold code (a "hello world" starter) but it's not the user's app yet. Focus on understanding what they want to build, not on the existing code.
|
|
383
|
-
- **
|
|
384
|
-
- **
|
|
373
|
+
- **intake**: Gathering requirements. The project has scaffold code (a "hello world" starter) but it's not the user's app yet. Focus on understanding what they want to build, not on the existing code. Intake ends with a plan proposal via writePlan.
|
|
374
|
+
- **building**: The user approved the initial plan. The agent is writing the spec and building the app. This can take a while and involves heavy tool use (spec authoring, design expert consultation, code generation, verification, polishing).
|
|
375
|
+
- **buildComplete**: The build is done. The frontend is showing the user a reveal experience (pitch deck, app preview). The agent does not need to do anything in this state; the frontend advances to onboardingFinished when the user is ready.
|
|
385
376
|
- **onboardingFinished**: The project is built and ready. Full development mode with all tools available. From here on, keep spec and code in sync as changes are made.
|
|
386
377
|
</project_onboarding>
|
|
387
378
|
|
|
@@ -1298,7 +1289,7 @@ var writePlanTool = {
|
|
|
1298
1289
|
clearable: false,
|
|
1299
1290
|
definition: {
|
|
1300
1291
|
name: "writePlan",
|
|
1301
|
-
description: "Write an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. The plan is
|
|
1292
|
+
description: "Write an implementation plan for user approval before making changes. Use this only for large, multi-step changes like new features, new interface types, or when the user explicitly asks to see a plan. Most work should be done autonomously without a plan. Write a clear markdown summary of what you intend to do in plain language \u2014 describe the changes from the user's perspective, not as a list of files and code paths. The plan is displayed standalone in the UI with approve/reject buttons, so write only the plan itself \u2014 no conversational text, no 'what do you think?', no next-steps narration. Say those things in your chat message instead. If the user asks for revisions, call this tool again with updated content to overwrite the plan.",
|
|
1302
1293
|
inputSchema: {
|
|
1303
1294
|
type: "object",
|
|
1304
1295
|
properties: {
|
|
@@ -1368,13 +1359,13 @@ var setProjectOnboardingStateTool = {
|
|
|
1368
1359
|
clearable: false,
|
|
1369
1360
|
definition: {
|
|
1370
1361
|
name: "setProjectOnboardingState",
|
|
1371
|
-
description: "Advance the project onboarding state.
|
|
1362
|
+
description: "Advance the project onboarding state. Only call this when an automated action explicitly instructs you to \u2014 calling it at the wrong time skips stages the user hasn't experienced. Forward-only: building \u2192 buildComplete \u2192 onboardingFinished. `onboardingFinished` is set by the frontend after the user dismisses the reveal; do not call it yourself.",
|
|
1372
1363
|
inputSchema: {
|
|
1373
1364
|
type: "object",
|
|
1374
1365
|
properties: {
|
|
1375
1366
|
state: {
|
|
1376
1367
|
type: "string",
|
|
1377
|
-
enum: ["
|
|
1368
|
+
enum: ["building", "buildComplete", "onboardingFinished"],
|
|
1378
1369
|
description: "The onboarding state to advance to."
|
|
1379
1370
|
}
|
|
1380
1371
|
},
|
|
@@ -2541,7 +2532,7 @@ var runMethodTool = {
|
|
|
2541
2532
|
clearable: true,
|
|
2542
2533
|
definition: {
|
|
2543
2534
|
name: "runMethod",
|
|
2544
|
-
description:
|
|
2535
|
+
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.',
|
|
2545
2536
|
inputSchema: {
|
|
2546
2537
|
type: "object",
|
|
2547
2538
|
properties: {
|
|
@@ -2553,14 +2544,14 @@ var runMethodTool = {
|
|
|
2553
2544
|
type: "object",
|
|
2554
2545
|
description: "The input payload to pass to the method. Omit for methods that take no input."
|
|
2555
2546
|
},
|
|
2547
|
+
userId: {
|
|
2548
|
+
type: "string",
|
|
2549
|
+
description: 'Optional. Run the method as a specific user. Pass "testUser" to auto-auth as the default test user (the sandbox handles user creation/lookup \u2014 no scenario setup needed). Or pass a real user ID from scenario-seeded data for a specific user. Overrides session-level impersonation for this call only.'
|
|
2550
|
+
},
|
|
2556
2551
|
roles: {
|
|
2557
2552
|
type: "array",
|
|
2558
2553
|
items: { type: "string" },
|
|
2559
|
-
description: 'Optional. Role names for this request (e.g. ["admin"]).
|
|
2560
|
-
},
|
|
2561
|
-
userId: {
|
|
2562
|
-
type: "string",
|
|
2563
|
-
description: "Optional. User ID for this request \u2014 use a managed user's ID to simulate their identity. Overrides session-level impersonation for this call only."
|
|
2554
|
+
description: 'Optional. Role names for this request (e.g. ["admin"]). Combine with `userId` to test a specific role, or use alone to test role-gated logic without a full identity. Overrides session-level impersonation for this call only.'
|
|
2564
2555
|
}
|
|
2565
2556
|
},
|
|
2566
2557
|
required: ["method"]
|
|
@@ -2608,10 +2599,10 @@ async function analyzeImage(params) {
|
|
|
2608
2599
|
}
|
|
2609
2600
|
|
|
2610
2601
|
// src/tools/_helpers/screenshot.ts
|
|
2611
|
-
var SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components)
|
|
2602
|
+
var SCREENSHOT_ANALYSIS_PROMPT = `Describe everything visible on screen from top to bottom \u2014 every element, its position, its size relative to the viewport, its colors, its content. Be comprehensive, thorough, and spatial. After the inventory, note anything that looks visually broken (overlapping elements, clipped text, misaligned components).`;
|
|
2603
|
+
var TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.
|
|
2612
2604
|
|
|
2613
2605
|
Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
|
|
2614
|
-
var TEXT_WRAP_DISCLAIMER = `Note: ignore text wrapping issues. Screenshots occasionally show text wrapping onto an extra line compared to the live page \u2014 most noticeable in buttons, badges, and headings. This is a known limitation of SVG foreignObject rendering used the DOM-to-image capture library that took the screenshot. The browser's SVG renderer computes slightly wider text metrics than the HTML layout engine, so text that fits on one line in the live DOM can overflow by a fraction of a pixel in the capture - this is not a real issue.`;
|
|
2615
2606
|
function buildScreenshotAnalysisPrompt(opts) {
|
|
2616
2607
|
let p = opts?.prompt || SCREENSHOT_ANALYSIS_PROMPT;
|
|
2617
2608
|
if (opts?.styleMap) {
|
|
@@ -2695,23 +2686,22 @@ async function checkBrowserConnected() {
|
|
|
2695
2686
|
if (!status.connected) {
|
|
2696
2687
|
return {
|
|
2697
2688
|
connected: false,
|
|
2698
|
-
|
|
2689
|
+
reason: BROWSER_UNAVAILABLE_MESSAGE
|
|
2699
2690
|
};
|
|
2700
2691
|
}
|
|
2701
2692
|
return { connected: true };
|
|
2702
|
-
} catch
|
|
2693
|
+
} catch {
|
|
2703
2694
|
return {
|
|
2704
2695
|
connected: false,
|
|
2705
|
-
|
|
2696
|
+
reason: BROWSER_UNAVAILABLE_MESSAGE
|
|
2706
2697
|
};
|
|
2707
2698
|
}
|
|
2708
2699
|
}
|
|
2700
|
+
var BROWSER_UNAVAILABLE_MESSAGE = "Browser preview unavailable \u2014 the user has closed their browser and we are continuing to work in the background. This is not a code failure and not something to diagnose. Do not tell the user to click or open anything. Skip the visual check and verify your work through other means: runMethod for backend behavior, queryDatabase for data checks, .logs/devServer.ndjson for build errors, .logs/browser.ndjson for runtime errors, lspDiagnostics for type/syntax, or read the code directly.";
|
|
2709
2701
|
|
|
2710
2702
|
// src/statusWatcher.ts
|
|
2711
2703
|
function startStatusWatcher(config) {
|
|
2712
|
-
const { apiConfig, getContext, onStatus, interval =
|
|
2713
|
-
let lastLabel = "";
|
|
2714
|
-
let lastContext = "";
|
|
2704
|
+
const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
|
|
2715
2705
|
let inflight = false;
|
|
2716
2706
|
let stopped = false;
|
|
2717
2707
|
const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
|
|
@@ -2722,10 +2712,9 @@ function startStatusWatcher(config) {
|
|
|
2722
2712
|
inflight = true;
|
|
2723
2713
|
try {
|
|
2724
2714
|
const context = getContext();
|
|
2725
|
-
if (!context
|
|
2715
|
+
if (!context) {
|
|
2726
2716
|
return;
|
|
2727
2717
|
}
|
|
2728
|
-
lastContext = context;
|
|
2729
2718
|
const res = await fetch(url, {
|
|
2730
2719
|
method: "POST",
|
|
2731
2720
|
headers: {
|
|
@@ -2735,15 +2724,11 @@ function startStatusWatcher(config) {
|
|
|
2735
2724
|
body: JSON.stringify({ context }),
|
|
2736
2725
|
signal
|
|
2737
2726
|
});
|
|
2738
|
-
if (!res.ok) {
|
|
2727
|
+
if (!res.ok || stopped) {
|
|
2739
2728
|
return;
|
|
2740
2729
|
}
|
|
2741
2730
|
const data = await res.json();
|
|
2742
|
-
if (!data.label
|
|
2743
|
-
return;
|
|
2744
|
-
}
|
|
2745
|
-
lastLabel = data.label;
|
|
2746
|
-
if (stopped) {
|
|
2731
|
+
if (!data.label) {
|
|
2747
2732
|
return;
|
|
2748
2733
|
}
|
|
2749
2734
|
onStatus(data.label);
|
|
@@ -2763,6 +2748,43 @@ function startStatusWatcher(config) {
|
|
|
2763
2748
|
};
|
|
2764
2749
|
}
|
|
2765
2750
|
|
|
2751
|
+
// src/automatedActions/sentinel.ts
|
|
2752
|
+
function sentinel(name) {
|
|
2753
|
+
return `@@automated::${name}@@`;
|
|
2754
|
+
}
|
|
2755
|
+
function automatedMessage(name, body) {
|
|
2756
|
+
return body ? `${sentinel(name)}
|
|
2757
|
+
${body}` : sentinel(name);
|
|
2758
|
+
}
|
|
2759
|
+
function hasSentinel(text, name) {
|
|
2760
|
+
return text.startsWith(sentinel(name));
|
|
2761
|
+
}
|
|
2762
|
+
function isAutomatedMessage(text) {
|
|
2763
|
+
return text.startsWith("@@automated::");
|
|
2764
|
+
}
|
|
2765
|
+
function parseSentinel(text) {
|
|
2766
|
+
const match = text.match(/^@@automated::(\w+)@@(.*)/s);
|
|
2767
|
+
if (!match) {
|
|
2768
|
+
return null;
|
|
2769
|
+
}
|
|
2770
|
+
return { name: match[1], remainder: match[2] };
|
|
2771
|
+
}
|
|
2772
|
+
function stripSentinelLine(text) {
|
|
2773
|
+
return text.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "");
|
|
2774
|
+
}
|
|
2775
|
+
function buildBackgroundResultsMessage(results) {
|
|
2776
|
+
const xml = results.map(
|
|
2777
|
+
(r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
|
|
2778
|
+
${r.result}
|
|
2779
|
+
</tool_result>`
|
|
2780
|
+
).join("\n\n");
|
|
2781
|
+
const body = `This is an automated message containing the result of a tool call that has been working in the background. This is not a direct message from the user.
|
|
2782
|
+
<background_results>
|
|
2783
|
+
${xml}
|
|
2784
|
+
</background_results>`;
|
|
2785
|
+
return automatedMessage("background_results", body);
|
|
2786
|
+
}
|
|
2787
|
+
|
|
2766
2788
|
// src/subagents/common/cleanMessages.ts
|
|
2767
2789
|
function findLastSummaryCheckpoint(messages, name) {
|
|
2768
2790
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -2856,11 +2878,8 @@ ${summaryBlock.text}
|
|
|
2856
2878
|
}
|
|
2857
2879
|
return true;
|
|
2858
2880
|
}).map((msg) => {
|
|
2859
|
-
if (msg.role === "user" && typeof msg.content === "string" && msg.content
|
|
2860
|
-
return {
|
|
2861
|
-
...msg,
|
|
2862
|
-
content: msg.content.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "")
|
|
2863
|
-
};
|
|
2881
|
+
if (msg.role === "user" && typeof msg.content === "string" && isAutomatedMessage(msg.content)) {
|
|
2882
|
+
return { ...msg, content: stripSentinelLine(msg.content) };
|
|
2864
2883
|
}
|
|
2865
2884
|
if (!Array.isArray(msg.content)) {
|
|
2866
2885
|
return msg;
|
|
@@ -2918,7 +2937,7 @@ async function runSubAgent(config) {
|
|
|
2918
2937
|
const agentName = subAgentId || "sub-agent";
|
|
2919
2938
|
const runStart = Date.now();
|
|
2920
2939
|
log5.info("Sub-agent started", { requestId, parentToolId, agentName });
|
|
2921
|
-
const
|
|
2940
|
+
const emit = (e) => {
|
|
2922
2941
|
onEvent({ ...e, parentToolId });
|
|
2923
2942
|
};
|
|
2924
2943
|
const dateStr = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
|
|
@@ -2983,7 +3002,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
2983
3002
|
}
|
|
2984
3003
|
return parts.join("\n");
|
|
2985
3004
|
},
|
|
2986
|
-
onStatus: (label) =>
|
|
3005
|
+
onStatus: (label) => emit({ type: "status", message: label }),
|
|
2987
3006
|
signal
|
|
2988
3007
|
});
|
|
2989
3008
|
try {
|
|
@@ -3000,7 +3019,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3000
3019
|
signal
|
|
3001
3020
|
},
|
|
3002
3021
|
{
|
|
3003
|
-
onRetry: (attempt) =>
|
|
3022
|
+
onRetry: (attempt) => emit({
|
|
3004
3023
|
type: "status",
|
|
3005
3024
|
message: `Lost connection, retrying (attempt ${attempt + 2} of 3)...`
|
|
3006
3025
|
})
|
|
@@ -3021,14 +3040,14 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3021
3040
|
startedAt: event.ts
|
|
3022
3041
|
});
|
|
3023
3042
|
}
|
|
3024
|
-
|
|
3043
|
+
emit({ type: "text", text: event.text });
|
|
3025
3044
|
break;
|
|
3026
3045
|
}
|
|
3027
3046
|
case "thinking":
|
|
3028
3047
|
if (!thinkingStartedAt) {
|
|
3029
3048
|
thinkingStartedAt = event.ts;
|
|
3030
3049
|
}
|
|
3031
|
-
|
|
3050
|
+
emit({ type: "thinking", text: event.text });
|
|
3032
3051
|
break;
|
|
3033
3052
|
case "thinking_complete":
|
|
3034
3053
|
contentBlocks.push({
|
|
@@ -3048,7 +3067,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3048
3067
|
input: event.input,
|
|
3049
3068
|
startedAt: Date.now()
|
|
3050
3069
|
});
|
|
3051
|
-
|
|
3070
|
+
emit({
|
|
3052
3071
|
type: "tool_start",
|
|
3053
3072
|
id: event.id,
|
|
3054
3073
|
name: event.name,
|
|
@@ -3125,7 +3144,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3125
3144
|
if (externalTools.has(tc.name) && resolveExternalTool) {
|
|
3126
3145
|
result = await resolveExternalTool(tc.id, tc.name, input);
|
|
3127
3146
|
} else {
|
|
3128
|
-
const onLog = (line) =>
|
|
3147
|
+
const onLog = (line) => emit({
|
|
3129
3148
|
type: "tool_input_delta",
|
|
3130
3149
|
id: tc.id,
|
|
3131
3150
|
name: tc.name,
|
|
@@ -3176,7 +3195,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3176
3195
|
durationMs: Date.now() - toolStart,
|
|
3177
3196
|
isError: r.isError
|
|
3178
3197
|
});
|
|
3179
|
-
|
|
3198
|
+
emit({
|
|
3180
3199
|
type: "tool_done",
|
|
3181
3200
|
id: tc.id,
|
|
3182
3201
|
name: tc.name,
|
|
@@ -3445,7 +3464,7 @@ var browserAutomationTool = {
|
|
|
3445
3464
|
try {
|
|
3446
3465
|
const browserStatus = await checkBrowserConnected();
|
|
3447
3466
|
if (!browserStatus.connected) {
|
|
3448
|
-
return
|
|
3467
|
+
return browserStatus.reason ?? "Browser preview unavailable.";
|
|
3449
3468
|
}
|
|
3450
3469
|
try {
|
|
3451
3470
|
await sidecarRequest("/reset-browser", {}, { timeout: 5e3 });
|
|
@@ -3631,7 +3650,7 @@ var screenshotTool = {
|
|
|
3631
3650
|
try {
|
|
3632
3651
|
const browserStatus = await checkBrowserConnected();
|
|
3633
3652
|
if (!browserStatus.connected) {
|
|
3634
|
-
return
|
|
3653
|
+
return browserStatus.reason ?? "Browser preview unavailable.";
|
|
3635
3654
|
}
|
|
3636
3655
|
return await captureAndAnalyzeScreenshot({
|
|
3637
3656
|
prompt: input.prompt,
|
|
@@ -3969,7 +3988,7 @@ async function execute5(input, onLog, context) {
|
|
|
3969
3988
|
try {
|
|
3970
3989
|
const browserStatus = await checkBrowserConnected();
|
|
3971
3990
|
if (!browserStatus.connected) {
|
|
3972
|
-
return
|
|
3991
|
+
return browserStatus.reason ?? "Browser preview unavailable.";
|
|
3973
3992
|
}
|
|
3974
3993
|
return await captureAndAnalyzeScreenshot({
|
|
3975
3994
|
prompt: input.prompt,
|
|
@@ -4402,7 +4421,7 @@ Each interface type invokes the same backend methods. Methods don't know which i
|
|
|
4402
4421
|
TypeScript running in a sandboxed environment. Any npm package can be installed. Key capabilities:
|
|
4403
4422
|
|
|
4404
4423
|
- Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
|
|
4405
|
-
- Built-in app-managed auth. Opt-in via manifest \u2014 developer builds login UI, platform handles verification codes (email
|
|
4424
|
+
- Built-in app-managed auth. Opt-in via manifest \u2014 developer builds login UI, platform handles verification codes (email-code, sms-code) and cookie sessions. API key auth for programmatic access. No OAuth, no social login (no Apple, Google, Facebook, or GitHub sign-in). Backend methods use auth.requireRole() for access control.
|
|
4406
4425
|
- Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
|
|
4407
4426
|
- Git-native deployment. Push to default branch to deploy.
|
|
4408
4427
|
|
|
@@ -4834,7 +4853,7 @@ Use <current_deck> as your starting point and replace or update the content as n
|
|
|
4834
4853
|
- The deck must be a single HTML file \u2014 it will be rendered in an iframe.
|
|
4835
4854
|
- Must look beautiful on desktop and mobile.
|
|
4836
4855
|
- Animation between slides must be seamless, no flicker or flashing. For reveal animations: hide elements with CSS \`opacity: 0\` only (no transform in CSS). Let GSAP handle transforms via inline styles and never use \`clearProps\`. Use the existing scaffold, do not write your own transition logic or slide mechanics.
|
|
4837
|
-
- Be bold and impactful.
|
|
4856
|
+
- Be bold and impactful. Use images from the spec or generate new images when needed.
|
|
4838
4857
|
- Code must be clean, bug free, and easy to parse. Use GSAP for animations. Pay close attention to layout and alignment to make sure everything is perfect.
|
|
4839
4858
|
- Keep the progress bar and edge chevrons from the shell \u2014 they are part of the navigation UX.
|
|
4840
4859
|
|
|
@@ -5532,20 +5551,23 @@ async function runTurn(params) {
|
|
|
5532
5551
|
parts.push(`Tool: ${toolName}`);
|
|
5533
5552
|
}
|
|
5534
5553
|
if (lastCompletedInput) {
|
|
5535
|
-
parts.push(`Tool input: ${lastCompletedInput.slice(-
|
|
5554
|
+
parts.push(`Tool input: ${lastCompletedInput.slice(-1500)}`);
|
|
5536
5555
|
}
|
|
5537
5556
|
if (lastCompletedResult) {
|
|
5538
|
-
parts.push(`Tool result: ${lastCompletedResult.slice(-
|
|
5557
|
+
parts.push(`Tool result: ${lastCompletedResult.slice(-1500)}`);
|
|
5539
5558
|
}
|
|
5540
|
-
const text = subAgentText || getTextContent(contentBlocks).slice(-
|
|
5559
|
+
const text = subAgentText || getTextContent(contentBlocks).slice(-2e3);
|
|
5541
5560
|
if (text) {
|
|
5542
5561
|
parts.push(`Assistant text: ${text}`);
|
|
5543
5562
|
}
|
|
5544
5563
|
if (onboardingState && onboardingState !== "onboardingFinished") {
|
|
5545
5564
|
parts.push(`Build phase: ${onboardingState}`);
|
|
5546
5565
|
}
|
|
5547
|
-
|
|
5548
|
-
|
|
5566
|
+
const automated = parseSentinel(userMessage);
|
|
5567
|
+
if (automated) {
|
|
5568
|
+
parts.push(`Automated action: ${automated.name}`);
|
|
5569
|
+
} else if (userMessage) {
|
|
5570
|
+
parts.push(`User request: ${userMessage.slice(-500)}`);
|
|
5549
5571
|
}
|
|
5550
5572
|
return parts.join("\n");
|
|
5551
5573
|
},
|
|
@@ -6005,19 +6027,242 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
|
|
|
6005
6027
|
}
|
|
6006
6028
|
};
|
|
6007
6029
|
|
|
6030
|
+
// src/headless/attachments.ts
|
|
6031
|
+
import { mkdirSync, existsSync } from "fs";
|
|
6032
|
+
import { writeFile } from "fs/promises";
|
|
6033
|
+
import { basename, join, extname } from "path";
|
|
6034
|
+
var log11 = createLogger("headless:attachments");
|
|
6035
|
+
var UPLOADS_DIR = "src/.user-uploads";
|
|
6036
|
+
function filenameFromUrl(url) {
|
|
6037
|
+
try {
|
|
6038
|
+
const pathname = new URL(url).pathname;
|
|
6039
|
+
const name = basename(pathname);
|
|
6040
|
+
return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
|
|
6041
|
+
} catch {
|
|
6042
|
+
return `upload-${Date.now()}`;
|
|
6043
|
+
}
|
|
6044
|
+
}
|
|
6045
|
+
function resolveUniqueFilename(name) {
|
|
6046
|
+
if (!existsSync(join(UPLOADS_DIR, name))) {
|
|
6047
|
+
return name;
|
|
6048
|
+
}
|
|
6049
|
+
const ext = extname(name);
|
|
6050
|
+
const base = name.slice(0, name.length - ext.length);
|
|
6051
|
+
let counter = 1;
|
|
6052
|
+
while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
|
|
6053
|
+
counter++;
|
|
6054
|
+
}
|
|
6055
|
+
return `${base}-${counter}${ext}`;
|
|
6056
|
+
}
|
|
6057
|
+
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6058
|
+
".png",
|
|
6059
|
+
".jpg",
|
|
6060
|
+
".jpeg",
|
|
6061
|
+
".gif",
|
|
6062
|
+
".webp",
|
|
6063
|
+
".svg",
|
|
6064
|
+
".bmp",
|
|
6065
|
+
".ico",
|
|
6066
|
+
".tiff",
|
|
6067
|
+
".tif",
|
|
6068
|
+
".avif",
|
|
6069
|
+
".heic",
|
|
6070
|
+
".heif"
|
|
6071
|
+
]);
|
|
6072
|
+
function isImageAttachment(att) {
|
|
6073
|
+
const name = att.filename || filenameFromUrl(att.url);
|
|
6074
|
+
return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
|
|
6075
|
+
}
|
|
6076
|
+
async function persistAttachments(attachments) {
|
|
6077
|
+
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
6078
|
+
if (nonVoice.length === 0) {
|
|
6079
|
+
return { documents: [], images: [] };
|
|
6080
|
+
}
|
|
6081
|
+
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
6082
|
+
const results = await Promise.allSettled(
|
|
6083
|
+
nonVoice.map(async (att) => {
|
|
6084
|
+
const name = resolveUniqueFilename(
|
|
6085
|
+
att.filename || filenameFromUrl(att.url)
|
|
6086
|
+
);
|
|
6087
|
+
const localPath = join(UPLOADS_DIR, name);
|
|
6088
|
+
const res = await fetch(att.url, {
|
|
6089
|
+
signal: AbortSignal.timeout(3e4)
|
|
6090
|
+
});
|
|
6091
|
+
if (!res.ok) {
|
|
6092
|
+
throw new Error(`HTTP ${res.status} downloading ${att.url}`);
|
|
6093
|
+
}
|
|
6094
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
6095
|
+
await writeFile(localPath, buffer);
|
|
6096
|
+
log11.info("Attachment saved", {
|
|
6097
|
+
filename: name,
|
|
6098
|
+
path: localPath,
|
|
6099
|
+
bytes: buffer.length
|
|
6100
|
+
});
|
|
6101
|
+
let extractedTextPath;
|
|
6102
|
+
if (att.extractedTextUrl) {
|
|
6103
|
+
try {
|
|
6104
|
+
const textRes = await fetch(att.extractedTextUrl, {
|
|
6105
|
+
signal: AbortSignal.timeout(3e4)
|
|
6106
|
+
});
|
|
6107
|
+
if (textRes.ok) {
|
|
6108
|
+
extractedTextPath = `${localPath}.txt`;
|
|
6109
|
+
await writeFile(extractedTextPath, await textRes.text(), "utf-8");
|
|
6110
|
+
log11.info("Extracted text saved", { path: extractedTextPath });
|
|
6111
|
+
}
|
|
6112
|
+
} catch {
|
|
6113
|
+
}
|
|
6114
|
+
}
|
|
6115
|
+
return {
|
|
6116
|
+
filename: name,
|
|
6117
|
+
localPath,
|
|
6118
|
+
remoteUrl: att.url,
|
|
6119
|
+
extractedTextPath
|
|
6120
|
+
};
|
|
6121
|
+
})
|
|
6122
|
+
);
|
|
6123
|
+
const settled = results.map((r, i) => ({
|
|
6124
|
+
result: r.status === "fulfilled" ? r.value : null,
|
|
6125
|
+
isImage: isImageAttachment(nonVoice[i])
|
|
6126
|
+
}));
|
|
6127
|
+
return {
|
|
6128
|
+
documents: settled.filter((s) => !s.isImage).map((s) => s.result),
|
|
6129
|
+
images: settled.filter((s) => s.isImage).map((s) => s.result)
|
|
6130
|
+
};
|
|
6131
|
+
}
|
|
6132
|
+
function buildUploadHeader(results) {
|
|
6133
|
+
const succeeded = results.filter(Boolean);
|
|
6134
|
+
if (succeeded.length === 0) {
|
|
6135
|
+
return "";
|
|
6136
|
+
}
|
|
6137
|
+
if (succeeded.length === 1) {
|
|
6138
|
+
const r = succeeded[0];
|
|
6139
|
+
const parts = [`[Uploaded file: ${r.localPath} (CDN: ${r.remoteUrl})`];
|
|
6140
|
+
if (r.extractedTextPath) {
|
|
6141
|
+
parts.push(`extracted text: ${r.extractedTextPath}`);
|
|
6142
|
+
}
|
|
6143
|
+
return parts.join(" \u2014 ") + "]";
|
|
6144
|
+
}
|
|
6145
|
+
const lines = succeeded.map((r) => {
|
|
6146
|
+
const parts = [`- ${r.localPath} (CDN: ${r.remoteUrl})`];
|
|
6147
|
+
if (r.extractedTextPath) {
|
|
6148
|
+
parts.push(` extracted text: ${r.extractedTextPath}`);
|
|
6149
|
+
}
|
|
6150
|
+
return parts.join("\n");
|
|
6151
|
+
});
|
|
6152
|
+
return `[Uploaded files]
|
|
6153
|
+
${lines.join("\n")}`;
|
|
6154
|
+
}
|
|
6155
|
+
|
|
6156
|
+
// src/headless/planFile.ts
|
|
6157
|
+
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
6158
|
+
var PLAN_FILE3 = ".remy-plan.md";
|
|
6159
|
+
function applyPlanFileSideEffect(rawText) {
|
|
6160
|
+
if (hasSentinel(rawText, "approvePlan") || hasSentinel(rawText, "approveInitialPlan")) {
|
|
6161
|
+
try {
|
|
6162
|
+
const plan = readFileSync(PLAN_FILE3, "utf-8");
|
|
6163
|
+
writeFileSync(
|
|
6164
|
+
PLAN_FILE3,
|
|
6165
|
+
plan.replace(/^status:\s*pending/m, "status: approved"),
|
|
6166
|
+
"utf-8"
|
|
6167
|
+
);
|
|
6168
|
+
} catch {
|
|
6169
|
+
}
|
|
6170
|
+
} else if (hasSentinel(rawText, "rejectPlan")) {
|
|
6171
|
+
try {
|
|
6172
|
+
unlinkSync(PLAN_FILE3);
|
|
6173
|
+
} catch {
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
|
|
6178
|
+
// src/headless/stats.ts
|
|
6179
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6180
|
+
var STATS_FILE = ".remy-stats.json";
|
|
6181
|
+
function createSessionStats() {
|
|
6182
|
+
return {
|
|
6183
|
+
messageCount: 0,
|
|
6184
|
+
turns: 0,
|
|
6185
|
+
totalInputTokens: 0,
|
|
6186
|
+
totalOutputTokens: 0,
|
|
6187
|
+
totalCacheCreationTokens: 0,
|
|
6188
|
+
totalCacheReadTokens: 0,
|
|
6189
|
+
lastContextSize: 0,
|
|
6190
|
+
compactionInProgress: false,
|
|
6191
|
+
updatedAt: 0
|
|
6192
|
+
};
|
|
6193
|
+
}
|
|
6194
|
+
function loadQueue() {
|
|
6195
|
+
try {
|
|
6196
|
+
const stats = JSON.parse(readFileSync2(STATS_FILE, "utf-8"));
|
|
6197
|
+
if (Array.isArray(stats.queue)) {
|
|
6198
|
+
return stats.queue;
|
|
6199
|
+
}
|
|
6200
|
+
} catch {
|
|
6201
|
+
}
|
|
6202
|
+
return [];
|
|
6203
|
+
}
|
|
6204
|
+
function writeStats(stats, queue) {
|
|
6205
|
+
try {
|
|
6206
|
+
writeFileSync2(
|
|
6207
|
+
STATS_FILE,
|
|
6208
|
+
JSON.stringify({
|
|
6209
|
+
...stats,
|
|
6210
|
+
queue
|
|
6211
|
+
})
|
|
6212
|
+
);
|
|
6213
|
+
} catch {
|
|
6214
|
+
}
|
|
6215
|
+
}
|
|
6216
|
+
|
|
6217
|
+
// src/headless/messageQueue.ts
|
|
6218
|
+
var MessageQueue = class {
|
|
6219
|
+
items = [];
|
|
6220
|
+
onChange;
|
|
6221
|
+
constructor(initial = [], onChange) {
|
|
6222
|
+
this.items = [...initial];
|
|
6223
|
+
this.onChange = onChange;
|
|
6224
|
+
}
|
|
6225
|
+
push(item) {
|
|
6226
|
+
this.items.push(item);
|
|
6227
|
+
this.onChange?.();
|
|
6228
|
+
}
|
|
6229
|
+
shift() {
|
|
6230
|
+
const item = this.items.shift();
|
|
6231
|
+
if (item) {
|
|
6232
|
+
this.onChange?.();
|
|
6233
|
+
}
|
|
6234
|
+
return item;
|
|
6235
|
+
}
|
|
6236
|
+
/** Remove and return all queued items. */
|
|
6237
|
+
drain() {
|
|
6238
|
+
if (this.items.length === 0) {
|
|
6239
|
+
return [];
|
|
6240
|
+
}
|
|
6241
|
+
const all = this.items.splice(0);
|
|
6242
|
+
this.onChange?.();
|
|
6243
|
+
return all;
|
|
6244
|
+
}
|
|
6245
|
+
/** Copy of current queue contents (for surfacing on events). */
|
|
6246
|
+
snapshot() {
|
|
6247
|
+
return [...this.items];
|
|
6248
|
+
}
|
|
6249
|
+
get length() {
|
|
6250
|
+
return this.items.length;
|
|
6251
|
+
}
|
|
6252
|
+
};
|
|
6253
|
+
|
|
6008
6254
|
// src/automatedActions/resolve.ts
|
|
6009
6255
|
var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
|
|
6010
6256
|
function resolveAction(text) {
|
|
6011
|
-
const
|
|
6012
|
-
if (!
|
|
6257
|
+
const parsed = parseSentinel(text);
|
|
6258
|
+
if (!parsed) {
|
|
6013
6259
|
return null;
|
|
6014
6260
|
}
|
|
6015
|
-
const triggerName =
|
|
6261
|
+
const { name: triggerName, remainder } = parsed;
|
|
6016
6262
|
if (NON_ACTION_SENTINELS.has(triggerName)) {
|
|
6017
6263
|
return null;
|
|
6018
6264
|
}
|
|
6019
6265
|
let params = {};
|
|
6020
|
-
const remainder = match[2];
|
|
6021
6266
|
if (remainder) {
|
|
6022
6267
|
try {
|
|
6023
6268
|
params = JSON.parse(remainder.split("\n")[0]);
|
|
@@ -6039,114 +6284,155 @@ function resolveAction(text) {
|
|
|
6039
6284
|
body = body.replaceAll(`{{${key}}}`, str);
|
|
6040
6285
|
}
|
|
6041
6286
|
return {
|
|
6042
|
-
message:
|
|
6043
|
-
${body}`,
|
|
6287
|
+
message: automatedMessage(triggerName, body),
|
|
6044
6288
|
next
|
|
6045
6289
|
};
|
|
6046
6290
|
}
|
|
6047
6291
|
|
|
6048
|
-
// src/headless.ts
|
|
6049
|
-
var
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6292
|
+
// src/headless/index.ts
|
|
6293
|
+
var log12 = createLogger("headless");
|
|
6294
|
+
var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
|
|
6295
|
+
var USER_FACING_TOOLS = /* @__PURE__ */ new Set([
|
|
6296
|
+
"promptUser",
|
|
6297
|
+
"confirmDestructiveAction",
|
|
6298
|
+
"presentPublishPlan"
|
|
6299
|
+
]);
|
|
6300
|
+
var HeadlessSession = class {
|
|
6301
|
+
// Configuration
|
|
6302
|
+
opts;
|
|
6303
|
+
config;
|
|
6304
|
+
// Conversation state
|
|
6305
|
+
state = createAgentState();
|
|
6306
|
+
sessionStats = createSessionStats();
|
|
6307
|
+
// Turn lifecycle
|
|
6308
|
+
running = false;
|
|
6309
|
+
currentAbort = null;
|
|
6310
|
+
/** RequestId of the in-flight message command — injected into streamed events. */
|
|
6311
|
+
currentRequestId;
|
|
6312
|
+
/** Guard: track whether terminal `completed` was already sent so we emit exactly one. */
|
|
6313
|
+
completedEmitted = false;
|
|
6314
|
+
turnStart = 0;
|
|
6315
|
+
/**
|
|
6316
|
+
* Onboarding state of the currently-running turn. Captured at runSingleTurn
|
|
6317
|
+
* start so onBackgroundComplete can enqueue background results with the
|
|
6318
|
+
* right state (the triggering turn's state, not a stale one).
|
|
6319
|
+
*/
|
|
6320
|
+
currentOnboardingState;
|
|
6321
|
+
/**
|
|
6322
|
+
* Unified message queue. Holds pending work to deliver after the current
|
|
6323
|
+
* turn completes: chained automated actions, background sub-agent results,
|
|
6324
|
+
* and user messages sent while a turn is running. Strict FIFO. Persisted
|
|
6325
|
+
* to .remy-stats.json so queued work survives process restarts.
|
|
6326
|
+
*/
|
|
6327
|
+
queue;
|
|
6328
|
+
// External tool bridge
|
|
6329
|
+
pendingTools = /* @__PURE__ */ new Map();
|
|
6330
|
+
earlyResults = /* @__PURE__ */ new Map();
|
|
6331
|
+
// Tool block updates from background completions (separate from the message queue)
|
|
6332
|
+
pendingBlockUpdates = [];
|
|
6333
|
+
// Tool lifecycle management — shared across all nesting depths
|
|
6334
|
+
toolRegistry = new ToolRegistry();
|
|
6335
|
+
// IO
|
|
6336
|
+
readline = null;
|
|
6337
|
+
constructor(opts = {}) {
|
|
6338
|
+
this.opts = opts;
|
|
6339
|
+
}
|
|
6340
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6341
|
+
// Lifecycle
|
|
6342
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6343
|
+
async start() {
|
|
6344
|
+
const stderrWrite = (...args) => {
|
|
6345
|
+
process.stderr.write(args.map(String).join(" ") + "\n");
|
|
6346
|
+
};
|
|
6347
|
+
console.log = stderrWrite;
|
|
6348
|
+
console.warn = stderrWrite;
|
|
6349
|
+
console.info = stderrWrite;
|
|
6350
|
+
if (this.opts.lspUrl) {
|
|
6351
|
+
setLspBaseUrl(this.opts.lspUrl);
|
|
6352
|
+
}
|
|
6353
|
+
this.config = resolveConfig({
|
|
6354
|
+
apiKey: this.opts.apiKey,
|
|
6355
|
+
baseUrl: this.opts.baseUrl
|
|
6356
|
+
});
|
|
6357
|
+
const resumed = loadSession(this.state);
|
|
6358
|
+
this.queue = new MessageQueue(loadQueue(), () => this.persistStats());
|
|
6359
|
+
if (resumed) {
|
|
6360
|
+
this.emit("session_restored", {
|
|
6361
|
+
messageCount: this.state.messages.length,
|
|
6362
|
+
...this.queueFields()
|
|
6363
|
+
});
|
|
6077
6364
|
}
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6365
|
+
this.toolRegistry.onEvent = this.onEvent;
|
|
6366
|
+
this.readline = createInterface({ input: process.stdin });
|
|
6367
|
+
this.readline.on("line", this.handleStdinLine);
|
|
6368
|
+
this.readline.on("close", () => {
|
|
6369
|
+
this.emit("stopping");
|
|
6370
|
+
this.emit("stopped");
|
|
6371
|
+
process.exit(0);
|
|
6372
|
+
});
|
|
6373
|
+
process.on("SIGTERM", this.shutdown);
|
|
6374
|
+
process.on("SIGINT", this.shutdown);
|
|
6375
|
+
this.emit("ready", this.queueFields());
|
|
6081
6376
|
}
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
process.
|
|
6086
|
-
};
|
|
6087
|
-
console.log = stderrWrite;
|
|
6088
|
-
console.warn = stderrWrite;
|
|
6089
|
-
console.info = stderrWrite;
|
|
6090
|
-
if (opts.lspUrl) {
|
|
6091
|
-
setLspBaseUrl(opts.lspUrl);
|
|
6092
|
-
}
|
|
6093
|
-
const config = resolveConfig({
|
|
6094
|
-
apiKey: opts.apiKey,
|
|
6095
|
-
baseUrl: opts.baseUrl
|
|
6096
|
-
});
|
|
6097
|
-
const state = createAgentState();
|
|
6098
|
-
const resumed = loadSession(state);
|
|
6099
|
-
if (resumed) {
|
|
6100
|
-
emit("session_restored", { messageCount: state.messages.length });
|
|
6101
|
-
}
|
|
6102
|
-
let running = false;
|
|
6103
|
-
let currentAbort = null;
|
|
6104
|
-
let currentRequestId;
|
|
6105
|
-
let completedEmitted = false;
|
|
6106
|
-
let turnStart = 0;
|
|
6107
|
-
let pendingNextAction;
|
|
6108
|
-
const EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
|
|
6109
|
-
const pendingTools = /* @__PURE__ */ new Map();
|
|
6110
|
-
const earlyResults = /* @__PURE__ */ new Map();
|
|
6111
|
-
const toolRegistry = new ToolRegistry();
|
|
6112
|
-
const sessionStats = {
|
|
6113
|
-
messageCount: 0,
|
|
6114
|
-
turns: 0,
|
|
6115
|
-
totalInputTokens: 0,
|
|
6116
|
-
totalOutputTokens: 0,
|
|
6117
|
-
totalCacheCreationTokens: 0,
|
|
6118
|
-
totalCacheReadTokens: 0,
|
|
6119
|
-
lastContextSize: 0,
|
|
6120
|
-
compactionInProgress: false,
|
|
6121
|
-
updatedAt: 0
|
|
6377
|
+
shutdown = () => {
|
|
6378
|
+
this.emit("stopping");
|
|
6379
|
+
this.emit("stopped");
|
|
6380
|
+
process.exit(0);
|
|
6122
6381
|
};
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6382
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6383
|
+
// Wire protocol
|
|
6384
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6385
|
+
emit(event, data, requestId) {
|
|
6386
|
+
const payload = { event, ...data };
|
|
6387
|
+
if (requestId) {
|
|
6388
|
+
payload.requestId = requestId;
|
|
6127
6389
|
}
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6390
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
6391
|
+
}
|
|
6392
|
+
/**
|
|
6393
|
+
* Emit a `completed` event and mark completedEmitted. Includes
|
|
6394
|
+
* `queuedMessages` if the queue has items (sandbox uses this to know the
|
|
6395
|
+
* agent is still busy with pipeline work).
|
|
6396
|
+
*/
|
|
6397
|
+
emitCompleted(rid, data) {
|
|
6398
|
+
this.emit("completed", { ...data, ...this.queueFields() }, rid);
|
|
6399
|
+
this.completedEmitted = true;
|
|
6400
|
+
}
|
|
6401
|
+
/** Returns `{ queuedMessages }` when the queue is non-empty, else empty object. */
|
|
6402
|
+
queueFields() {
|
|
6403
|
+
return this.queue.length > 0 ? { queuedMessages: this.queue.snapshot() } : {};
|
|
6141
6404
|
}
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6405
|
+
/** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
|
|
6406
|
+
dispatchSimple(requestId, eventName, handler) {
|
|
6407
|
+
try {
|
|
6408
|
+
const data = handler();
|
|
6409
|
+
if (eventName) {
|
|
6410
|
+
this.emit(eventName, data, requestId);
|
|
6411
|
+
}
|
|
6412
|
+
this.emit("completed", { success: true }, requestId);
|
|
6413
|
+
} catch (err) {
|
|
6414
|
+
this.emit("completed", { success: false, error: err.message }, requestId);
|
|
6415
|
+
}
|
|
6416
|
+
}
|
|
6417
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6418
|
+
// Stats + queue persistence
|
|
6419
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6420
|
+
/** Persist sessionStats + queue snapshot to .remy-stats.json. */
|
|
6421
|
+
persistStats() {
|
|
6422
|
+
this.sessionStats.updatedAt = Date.now();
|
|
6423
|
+
writeStats(this.sessionStats, this.queue.snapshot());
|
|
6424
|
+
}
|
|
6425
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6426
|
+
// Background completions (tool-block mutation; message delivery via queue)
|
|
6427
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6428
|
+
/** Apply queued tool block updates to state.messages. Safe to call any time. */
|
|
6429
|
+
applyPendingBlockUpdates() {
|
|
6430
|
+
if (this.pendingBlockUpdates.length === 0) {
|
|
6145
6431
|
return;
|
|
6146
6432
|
}
|
|
6147
|
-
const updates = pendingBlockUpdates.splice(0);
|
|
6433
|
+
const updates = this.pendingBlockUpdates.splice(0);
|
|
6148
6434
|
for (const update of updates) {
|
|
6149
|
-
for (const msg of state.messages) {
|
|
6435
|
+
for (const msg of this.state.messages) {
|
|
6150
6436
|
if (!Array.isArray(msg.content)) {
|
|
6151
6437
|
continue;
|
|
6152
6438
|
}
|
|
@@ -6161,60 +6447,65 @@ ${xmlParts}
|
|
|
6161
6447
|
}
|
|
6162
6448
|
}
|
|
6163
6449
|
}
|
|
6450
|
+
saveSession(this.state);
|
|
6164
6451
|
}
|
|
6165
|
-
|
|
6452
|
+
/** Drain pending compaction summaries and insert at a safe point. */
|
|
6453
|
+
applyPendingSummaries() {
|
|
6166
6454
|
const summaries = getPendingSummaries();
|
|
6167
6455
|
if (summaries.length === 0) {
|
|
6168
6456
|
return;
|
|
6169
6457
|
}
|
|
6170
|
-
const idx = findSafeInsertionPoint(state.messages);
|
|
6171
|
-
state.messages.splice(idx, 0, ...summaries);
|
|
6172
|
-
saveSession(state);
|
|
6458
|
+
const idx = findSafeInsertionPoint(this.state.messages);
|
|
6459
|
+
this.state.messages.splice(idx, 0, ...summaries);
|
|
6460
|
+
saveSession(this.state);
|
|
6173
6461
|
}
|
|
6174
|
-
|
|
6175
|
-
pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
|
|
6176
|
-
|
|
6462
|
+
onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
|
|
6463
|
+
this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
|
|
6464
|
+
log12.info("Background complete", {
|
|
6177
6465
|
toolCallId,
|
|
6178
6466
|
name,
|
|
6179
|
-
requestId: currentRequestId
|
|
6467
|
+
requestId: this.currentRequestId
|
|
6180
6468
|
});
|
|
6181
|
-
onEvent({
|
|
6469
|
+
this.onEvent({
|
|
6182
6470
|
type: "tool_background_complete",
|
|
6183
6471
|
id: toolCallId,
|
|
6184
6472
|
name,
|
|
6185
6473
|
result
|
|
6186
6474
|
});
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6475
|
+
this.queue.push({
|
|
6476
|
+
command: {
|
|
6477
|
+
action: "message",
|
|
6478
|
+
text: buildBackgroundResultsMessage([{ toolCallId, name, result }]),
|
|
6479
|
+
...this.currentOnboardingState && {
|
|
6480
|
+
onboardingState: this.currentOnboardingState
|
|
6481
|
+
}
|
|
6482
|
+
},
|
|
6483
|
+
source: "background",
|
|
6484
|
+
enqueuedAt: Date.now()
|
|
6192
6485
|
});
|
|
6193
|
-
if (!running) {
|
|
6194
|
-
applyPendingBlockUpdates();
|
|
6195
|
-
|
|
6486
|
+
if (!this.running) {
|
|
6487
|
+
this.applyPendingBlockUpdates();
|
|
6488
|
+
this.kickDrain();
|
|
6196
6489
|
}
|
|
6197
|
-
}
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
function resolveExternalTool(id, name, _input) {
|
|
6204
|
-
const early = earlyResults.get(id);
|
|
6490
|
+
};
|
|
6491
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6492
|
+
// External tool bridge
|
|
6493
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6494
|
+
resolveExternalTool = (id, name, _input) => {
|
|
6495
|
+
const early = this.earlyResults.get(id);
|
|
6205
6496
|
if (early !== void 0) {
|
|
6206
|
-
earlyResults.delete(id);
|
|
6497
|
+
this.earlyResults.delete(id);
|
|
6207
6498
|
return Promise.resolve(early);
|
|
6208
6499
|
}
|
|
6209
6500
|
const shouldTimeout = !USER_FACING_TOOLS.has(name);
|
|
6210
6501
|
return new Promise((resolve2) => {
|
|
6211
6502
|
const timeout = shouldTimeout ? setTimeout(() => {
|
|
6212
|
-
pendingTools.delete(id);
|
|
6503
|
+
this.pendingTools.delete(id);
|
|
6213
6504
|
resolve2(
|
|
6214
6505
|
"Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
|
|
6215
6506
|
);
|
|
6216
6507
|
}, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
|
|
6217
|
-
pendingTools.set(id, {
|
|
6508
|
+
this.pendingTools.set(id, {
|
|
6218
6509
|
resolve: (result) => {
|
|
6219
6510
|
clearTimeout(timeout);
|
|
6220
6511
|
resolve2(result);
|
|
@@ -6222,57 +6513,51 @@ ${xmlParts}
|
|
|
6222
6513
|
timeout
|
|
6223
6514
|
});
|
|
6224
6515
|
});
|
|
6225
|
-
}
|
|
6226
|
-
|
|
6227
|
-
|
|
6516
|
+
};
|
|
6517
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6518
|
+
// AgentEvent → wire protocol translation
|
|
6519
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6520
|
+
onEvent = (e) => {
|
|
6521
|
+
const rid = this.currentRequestId;
|
|
6228
6522
|
switch (e.type) {
|
|
6229
6523
|
case "turn_started":
|
|
6230
|
-
emit("turn_started", {}, rid);
|
|
6524
|
+
this.emit("turn_started", {}, rid);
|
|
6525
|
+
return;
|
|
6526
|
+
case "user_message":
|
|
6527
|
+
this.emit("user_message", { text: e.text }, rid);
|
|
6231
6528
|
return;
|
|
6232
|
-
// Terminal events — translate to `completed
|
|
6529
|
+
// Terminal events — translate to `completed`.
|
|
6530
|
+
// Post-turn queue drain happens in handleMessage AFTER runTurn returns,
|
|
6531
|
+
// so that `running` is held across the drain and no user message can
|
|
6532
|
+
// slip in mid-pipeline.
|
|
6233
6533
|
case "turn_done":
|
|
6234
|
-
completedEmitted = true;
|
|
6235
6534
|
if (e.stats) {
|
|
6236
|
-
sessionStats.turns++;
|
|
6237
|
-
sessionStats.totalInputTokens += e.stats.inputTokens;
|
|
6238
|
-
sessionStats.totalOutputTokens += e.stats.outputTokens;
|
|
6239
|
-
sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
|
|
6240
|
-
sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
|
|
6241
|
-
sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
|
|
6535
|
+
this.sessionStats.turns++;
|
|
6536
|
+
this.sessionStats.totalInputTokens += e.stats.inputTokens;
|
|
6537
|
+
this.sessionStats.totalOutputTokens += e.stats.outputTokens;
|
|
6538
|
+
this.sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
|
|
6539
|
+
this.sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
|
|
6540
|
+
this.sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
|
|
6242
6541
|
}
|
|
6243
|
-
sessionStats.messageCount = state.messages.length;
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
}
|
|
6249
|
-
|
|
6542
|
+
this.sessionStats.messageCount = this.state.messages.length;
|
|
6543
|
+
this.persistStats();
|
|
6544
|
+
this.emitCompleted(rid, {
|
|
6545
|
+
success: true,
|
|
6546
|
+
durationMs: Date.now() - this.turnStart
|
|
6547
|
+
});
|
|
6548
|
+
return;
|
|
6549
|
+
case "turn_cancelled": {
|
|
6550
|
+
this.emit(
|
|
6250
6551
|
"completed",
|
|
6251
|
-
{ success:
|
|
6552
|
+
{ success: false, error: "cancelled", ...this.queueFields() },
|
|
6252
6553
|
rid
|
|
6253
6554
|
);
|
|
6254
|
-
|
|
6255
|
-
applyPendingSummaries();
|
|
6256
|
-
applyPendingBlockUpdates();
|
|
6257
|
-
flushBackgroundQueue();
|
|
6258
|
-
if (pendingNextAction) {
|
|
6259
|
-
const next = pendingNextAction;
|
|
6260
|
-
pendingNextAction = void 0;
|
|
6261
|
-
handleMessage(
|
|
6262
|
-
{ action: "message", text: `@@automated::${next}@@` },
|
|
6263
|
-
`chain-${Date.now()}`
|
|
6264
|
-
);
|
|
6265
|
-
}
|
|
6266
|
-
}, 0);
|
|
6267
|
-
return;
|
|
6268
|
-
case "turn_cancelled":
|
|
6269
|
-
completedEmitted = true;
|
|
6270
|
-
pendingNextAction = void 0;
|
|
6271
|
-
emit("completed", { success: false, error: "cancelled" }, rid);
|
|
6555
|
+
this.completedEmitted = true;
|
|
6272
6556
|
return;
|
|
6557
|
+
}
|
|
6273
6558
|
// Streaming events — forward with requestId
|
|
6274
6559
|
case "text":
|
|
6275
|
-
emit(
|
|
6560
|
+
this.emit(
|
|
6276
6561
|
"text",
|
|
6277
6562
|
{
|
|
6278
6563
|
text: e.text,
|
|
@@ -6282,7 +6567,7 @@ ${xmlParts}
|
|
|
6282
6567
|
);
|
|
6283
6568
|
return;
|
|
6284
6569
|
case "thinking":
|
|
6285
|
-
emit(
|
|
6570
|
+
this.emit(
|
|
6286
6571
|
"thinking",
|
|
6287
6572
|
{
|
|
6288
6573
|
text: e.text,
|
|
@@ -6292,7 +6577,7 @@ ${xmlParts}
|
|
|
6292
6577
|
);
|
|
6293
6578
|
return;
|
|
6294
6579
|
case "tool_input_delta":
|
|
6295
|
-
emit(
|
|
6580
|
+
this.emit(
|
|
6296
6581
|
"tool_input_delta",
|
|
6297
6582
|
{
|
|
6298
6583
|
id: e.id,
|
|
@@ -6304,7 +6589,7 @@ ${xmlParts}
|
|
|
6304
6589
|
);
|
|
6305
6590
|
return;
|
|
6306
6591
|
case "tool_start":
|
|
6307
|
-
emit(
|
|
6592
|
+
this.emit(
|
|
6308
6593
|
"tool_start",
|
|
6309
6594
|
{
|
|
6310
6595
|
id: e.id,
|
|
@@ -6318,7 +6603,7 @@ ${xmlParts}
|
|
|
6318
6603
|
);
|
|
6319
6604
|
return;
|
|
6320
6605
|
case "tool_done":
|
|
6321
|
-
emit(
|
|
6606
|
+
this.emit(
|
|
6322
6607
|
"tool_done",
|
|
6323
6608
|
{
|
|
6324
6609
|
id: e.id,
|
|
@@ -6331,7 +6616,7 @@ ${xmlParts}
|
|
|
6331
6616
|
);
|
|
6332
6617
|
return;
|
|
6333
6618
|
case "tool_background_complete":
|
|
6334
|
-
emit(
|
|
6619
|
+
this.emit(
|
|
6335
6620
|
"tool_background_complete",
|
|
6336
6621
|
{
|
|
6337
6622
|
id: e.id,
|
|
@@ -6343,7 +6628,7 @@ ${xmlParts}
|
|
|
6343
6628
|
);
|
|
6344
6629
|
return;
|
|
6345
6630
|
case "tool_stopped":
|
|
6346
|
-
emit(
|
|
6631
|
+
this.emit(
|
|
6347
6632
|
"tool_stopped",
|
|
6348
6633
|
{
|
|
6349
6634
|
id: e.id,
|
|
@@ -6355,7 +6640,7 @@ ${xmlParts}
|
|
|
6355
6640
|
);
|
|
6356
6641
|
return;
|
|
6357
6642
|
case "tool_restarted":
|
|
6358
|
-
emit(
|
|
6643
|
+
this.emit(
|
|
6359
6644
|
"tool_restarted",
|
|
6360
6645
|
{
|
|
6361
6646
|
id: e.id,
|
|
@@ -6367,7 +6652,7 @@ ${xmlParts}
|
|
|
6367
6652
|
);
|
|
6368
6653
|
return;
|
|
6369
6654
|
case "status":
|
|
6370
|
-
emit(
|
|
6655
|
+
this.emit(
|
|
6371
6656
|
"status",
|
|
6372
6657
|
{
|
|
6373
6658
|
message: e.message,
|
|
@@ -6377,147 +6662,27 @@ ${xmlParts}
|
|
|
6377
6662
|
);
|
|
6378
6663
|
return;
|
|
6379
6664
|
case "error":
|
|
6380
|
-
emit("error", { error: e.error }, rid);
|
|
6665
|
+
this.emit("error", { error: e.error }, rid);
|
|
6381
6666
|
return;
|
|
6382
6667
|
}
|
|
6383
|
-
}
|
|
6384
|
-
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
|
|
6389
|
-
|
|
6390
|
-
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
|
|
6394
|
-
|
|
6395
|
-
|
|
6396
|
-
|
|
6397
|
-
|
|
6398
|
-
}
|
|
6399
|
-
const ext = extname(name);
|
|
6400
|
-
const base = name.slice(0, name.length - ext.length);
|
|
6401
|
-
let counter = 1;
|
|
6402
|
-
while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
|
|
6403
|
-
counter++;
|
|
6404
|
-
}
|
|
6405
|
-
return `${base}-${counter}${ext}`;
|
|
6406
|
-
}
|
|
6407
|
-
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6408
|
-
".png",
|
|
6409
|
-
".jpg",
|
|
6410
|
-
".jpeg",
|
|
6411
|
-
".gif",
|
|
6412
|
-
".webp",
|
|
6413
|
-
".svg",
|
|
6414
|
-
".bmp",
|
|
6415
|
-
".ico",
|
|
6416
|
-
".tiff",
|
|
6417
|
-
".tif",
|
|
6418
|
-
".avif",
|
|
6419
|
-
".heic",
|
|
6420
|
-
".heif"
|
|
6421
|
-
]);
|
|
6422
|
-
function isImageAttachment(att) {
|
|
6423
|
-
const name = att.filename || filenameFromUrl(att.url);
|
|
6424
|
-
return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
|
|
6425
|
-
}
|
|
6426
|
-
async function persistAttachments(attachments) {
|
|
6427
|
-
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
6428
|
-
if (nonVoice.length === 0) {
|
|
6429
|
-
return { documents: [], images: [] };
|
|
6430
|
-
}
|
|
6431
|
-
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
6432
|
-
const results = await Promise.allSettled(
|
|
6433
|
-
nonVoice.map(async (att) => {
|
|
6434
|
-
const name = resolveUniqueFilename(
|
|
6435
|
-
att.filename || filenameFromUrl(att.url)
|
|
6436
|
-
);
|
|
6437
|
-
const localPath = join(UPLOADS_DIR, name);
|
|
6438
|
-
const res = await fetch(att.url, {
|
|
6439
|
-
signal: AbortSignal.timeout(3e4)
|
|
6440
|
-
});
|
|
6441
|
-
if (!res.ok) {
|
|
6442
|
-
throw new Error(`HTTP ${res.status} downloading ${att.url}`);
|
|
6443
|
-
}
|
|
6444
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
6445
|
-
await writeFile(localPath, buffer);
|
|
6446
|
-
log11.info("Attachment saved", {
|
|
6447
|
-
filename: name,
|
|
6448
|
-
path: localPath,
|
|
6449
|
-
bytes: buffer.length
|
|
6450
|
-
});
|
|
6451
|
-
let extractedTextPath;
|
|
6452
|
-
if (att.extractedTextUrl) {
|
|
6453
|
-
try {
|
|
6454
|
-
const textRes = await fetch(att.extractedTextUrl, {
|
|
6455
|
-
signal: AbortSignal.timeout(3e4)
|
|
6456
|
-
});
|
|
6457
|
-
if (textRes.ok) {
|
|
6458
|
-
extractedTextPath = `${localPath}.txt`;
|
|
6459
|
-
await writeFile(extractedTextPath, await textRes.text(), "utf-8");
|
|
6460
|
-
log11.info("Extracted text saved", { path: extractedTextPath });
|
|
6461
|
-
}
|
|
6462
|
-
} catch {
|
|
6463
|
-
}
|
|
6464
|
-
}
|
|
6465
|
-
return { filename: name, localPath, extractedTextPath };
|
|
6466
|
-
})
|
|
6467
|
-
);
|
|
6468
|
-
const settled = results.map((r, i) => ({
|
|
6469
|
-
result: r.status === "fulfilled" ? r.value : null,
|
|
6470
|
-
isImage: isImageAttachment(nonVoice[i])
|
|
6471
|
-
}));
|
|
6472
|
-
return {
|
|
6473
|
-
documents: settled.filter((s) => !s.isImage).map((s) => s.result),
|
|
6474
|
-
images: settled.filter((s) => s.isImage).map((s) => s.result)
|
|
6475
|
-
};
|
|
6476
|
-
}
|
|
6477
|
-
function buildUploadHeader(results) {
|
|
6478
|
-
const succeeded = results.filter(Boolean);
|
|
6479
|
-
if (succeeded.length === 0) {
|
|
6480
|
-
return "";
|
|
6481
|
-
}
|
|
6482
|
-
if (succeeded.length === 1) {
|
|
6483
|
-
const r = succeeded[0];
|
|
6484
|
-
const parts = [`[Uploaded file: ${r.localPath}`];
|
|
6485
|
-
if (r.extractedTextPath) {
|
|
6486
|
-
parts.push(`extracted text: ${r.extractedTextPath}`);
|
|
6487
|
-
}
|
|
6488
|
-
return parts.join(" \u2014 ") + "]";
|
|
6489
|
-
}
|
|
6490
|
-
const lines = succeeded.map((r) => {
|
|
6491
|
-
if (r.extractedTextPath) {
|
|
6492
|
-
return `- ${r.localPath} (extracted text: ${r.extractedTextPath})`;
|
|
6493
|
-
}
|
|
6494
|
-
return `- ${r.localPath}`;
|
|
6495
|
-
});
|
|
6496
|
-
return `[Uploaded files]
|
|
6497
|
-
${lines.join("\n")}`;
|
|
6498
|
-
}
|
|
6499
|
-
async function handleMessage(parsed, requestId) {
|
|
6500
|
-
if (running) {
|
|
6501
|
-
emit(
|
|
6502
|
-
"error",
|
|
6503
|
-
{ error: "Agent is already processing a message" },
|
|
6504
|
-
requestId
|
|
6505
|
-
);
|
|
6506
|
-
emit(
|
|
6507
|
-
"completed",
|
|
6508
|
-
{ success: false, error: "Agent is already processing a message" },
|
|
6509
|
-
requestId
|
|
6510
|
-
);
|
|
6511
|
-
return;
|
|
6512
|
-
}
|
|
6513
|
-
running = true;
|
|
6514
|
-
currentRequestId = requestId;
|
|
6515
|
-
currentAbort = new AbortController();
|
|
6516
|
-
completedEmitted = false;
|
|
6517
|
-
turnStart = Date.now();
|
|
6668
|
+
};
|
|
6669
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6670
|
+
// Message command handler (long-running / streaming)
|
|
6671
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6672
|
+
/**
|
|
6673
|
+
* Run one turn (without acquiring the `running` lock). Called by
|
|
6674
|
+
* handleMessage for the initial turn, then repeatedly for each queued
|
|
6675
|
+
* message — `running` stays held across the queue drain so no user
|
|
6676
|
+
* message can slip in mid-pipeline.
|
|
6677
|
+
*/
|
|
6678
|
+
async runSingleTurn(parsed, requestId) {
|
|
6679
|
+
this.currentRequestId = requestId;
|
|
6680
|
+
this.currentAbort = new AbortController();
|
|
6681
|
+
this.completedEmitted = false;
|
|
6682
|
+
this.turnStart = Date.now();
|
|
6518
6683
|
const attachments = parsed.attachments;
|
|
6519
6684
|
if (attachments?.length) {
|
|
6520
|
-
|
|
6685
|
+
log12.info("Message has attachments", {
|
|
6521
6686
|
count: attachments.length,
|
|
6522
6687
|
urls: attachments.map((a) => a.url)
|
|
6523
6688
|
});
|
|
@@ -6534,143 +6699,242 @@ ${lines.join("\n")}`;
|
|
|
6534
6699
|
${userMessage}` : header;
|
|
6535
6700
|
}
|
|
6536
6701
|
} catch (err) {
|
|
6537
|
-
|
|
6702
|
+
log12.warn("Attachment persistence failed", { error: err.message });
|
|
6538
6703
|
}
|
|
6539
6704
|
}
|
|
6540
6705
|
let resolved = null;
|
|
6541
6706
|
try {
|
|
6542
6707
|
resolved = resolveAction(userMessage);
|
|
6543
6708
|
} catch (err) {
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
);
|
|
6709
|
+
this.emitCompleted(requestId, {
|
|
6710
|
+
success: false,
|
|
6711
|
+
error: err.message || "Failed to resolve action"
|
|
6712
|
+
});
|
|
6549
6713
|
return;
|
|
6550
6714
|
}
|
|
6551
|
-
pendingNextAction = void 0;
|
|
6552
6715
|
if (resolved !== null) {
|
|
6553
6716
|
userMessage = resolved.message;
|
|
6554
|
-
pendingNextAction = resolved.next;
|
|
6555
6717
|
}
|
|
6556
6718
|
const isHidden = resolved !== null || !!parsed.hidden;
|
|
6557
|
-
|
|
6558
|
-
if (rawText.startsWith("@@automated::approvePlan@@")) {
|
|
6559
|
-
try {
|
|
6560
|
-
const plan = readFileSync(".remy-plan.md", "utf-8");
|
|
6561
|
-
writeFileSync(
|
|
6562
|
-
".remy-plan.md",
|
|
6563
|
-
plan.replace(/^status:\s*pending/m, "status: approved"),
|
|
6564
|
-
"utf-8"
|
|
6565
|
-
);
|
|
6566
|
-
} catch {
|
|
6567
|
-
}
|
|
6568
|
-
} else if (rawText.startsWith("@@automated::rejectPlan@@")) {
|
|
6569
|
-
try {
|
|
6570
|
-
unlinkSync(".remy-plan.md");
|
|
6571
|
-
} catch {
|
|
6572
|
-
}
|
|
6573
|
-
}
|
|
6719
|
+
applyPlanFileSideEffect(parsed.text ?? "");
|
|
6574
6720
|
const onboardingState = parsed.onboardingState ?? "onboardingFinished";
|
|
6721
|
+
this.currentOnboardingState = onboardingState;
|
|
6575
6722
|
const system = buildSystemPrompt(
|
|
6576
6723
|
onboardingState,
|
|
6577
6724
|
parsed.viewContext
|
|
6578
6725
|
);
|
|
6726
|
+
if (resolved?.next) {
|
|
6727
|
+
this.queue.push({
|
|
6728
|
+
command: {
|
|
6729
|
+
action: "message",
|
|
6730
|
+
text: sentinel(resolved.next),
|
|
6731
|
+
onboardingState
|
|
6732
|
+
},
|
|
6733
|
+
source: "chain",
|
|
6734
|
+
enqueuedAt: Date.now()
|
|
6735
|
+
});
|
|
6736
|
+
}
|
|
6579
6737
|
try {
|
|
6580
6738
|
await runTurn({
|
|
6581
|
-
state,
|
|
6739
|
+
state: this.state,
|
|
6582
6740
|
userMessage,
|
|
6583
6741
|
attachments,
|
|
6584
|
-
apiConfig: config,
|
|
6742
|
+
apiConfig: this.config,
|
|
6585
6743
|
system,
|
|
6586
|
-
model: opts.model,
|
|
6744
|
+
model: this.opts.model,
|
|
6587
6745
|
onboardingState,
|
|
6588
6746
|
requestId,
|
|
6589
|
-
signal: currentAbort.signal,
|
|
6590
|
-
onEvent,
|
|
6591
|
-
resolveExternalTool,
|
|
6747
|
+
signal: this.currentAbort.signal,
|
|
6748
|
+
onEvent: this.onEvent,
|
|
6749
|
+
resolveExternalTool: this.resolveExternalTool,
|
|
6592
6750
|
hidden: isHidden,
|
|
6593
|
-
toolRegistry,
|
|
6594
|
-
onBackgroundComplete
|
|
6751
|
+
toolRegistry: this.toolRegistry,
|
|
6752
|
+
onBackgroundComplete: this.onBackgroundComplete
|
|
6595
6753
|
});
|
|
6596
|
-
if (!completedEmitted) {
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
);
|
|
6754
|
+
if (!this.completedEmitted) {
|
|
6755
|
+
this.emitCompleted(requestId, {
|
|
6756
|
+
success: false,
|
|
6757
|
+
error: "Turn ended unexpectedly"
|
|
6758
|
+
});
|
|
6602
6759
|
}
|
|
6603
|
-
|
|
6760
|
+
log12.info("Turn complete", {
|
|
6604
6761
|
requestId,
|
|
6605
|
-
durationMs: Date.now() - turnStart
|
|
6762
|
+
durationMs: Date.now() - this.turnStart
|
|
6606
6763
|
});
|
|
6607
6764
|
} catch (err) {
|
|
6608
|
-
if (!completedEmitted) {
|
|
6609
|
-
emit("error", { error: err.message }, requestId);
|
|
6610
|
-
|
|
6765
|
+
if (!this.completedEmitted) {
|
|
6766
|
+
this.emit("error", { error: err.message }, requestId);
|
|
6767
|
+
this.emitCompleted(requestId, {
|
|
6768
|
+
success: false,
|
|
6769
|
+
error: err.message
|
|
6770
|
+
});
|
|
6611
6771
|
}
|
|
6612
|
-
|
|
6772
|
+
log12.warn("Command failed", {
|
|
6613
6773
|
action: "message",
|
|
6614
6774
|
requestId,
|
|
6615
6775
|
error: err.message
|
|
6616
6776
|
});
|
|
6777
|
+
this.queue.drain();
|
|
6778
|
+
}
|
|
6779
|
+
this.applyPendingSummaries();
|
|
6780
|
+
this.applyPendingBlockUpdates();
|
|
6781
|
+
}
|
|
6782
|
+
async handleMessage(parsed, requestId) {
|
|
6783
|
+
if (this.running) {
|
|
6784
|
+
const command = { ...parsed };
|
|
6785
|
+
if (requestId && command.requestId === void 0) {
|
|
6786
|
+
command.requestId = requestId;
|
|
6787
|
+
}
|
|
6788
|
+
this.queue.push({
|
|
6789
|
+
command,
|
|
6790
|
+
source: "user",
|
|
6791
|
+
enqueuedAt: Date.now()
|
|
6792
|
+
});
|
|
6793
|
+
this.emit(
|
|
6794
|
+
"queued",
|
|
6795
|
+
{ position: this.queue.length, ...this.queueFields() },
|
|
6796
|
+
requestId
|
|
6797
|
+
);
|
|
6798
|
+
return;
|
|
6799
|
+
}
|
|
6800
|
+
this.running = true;
|
|
6801
|
+
try {
|
|
6802
|
+
await this.runSingleTurn(parsed, requestId);
|
|
6803
|
+
await this.drainQueueLoop();
|
|
6804
|
+
} finally {
|
|
6805
|
+
this.currentAbort = null;
|
|
6806
|
+
this.currentRequestId = void 0;
|
|
6807
|
+
this.running = false;
|
|
6808
|
+
}
|
|
6809
|
+
}
|
|
6810
|
+
/**
|
|
6811
|
+
* Drain the queue in strict FIFO order. Caller must hold `running = true`.
|
|
6812
|
+
* User messages arriving during the drain will be enqueued behind current items.
|
|
6813
|
+
*/
|
|
6814
|
+
async drainQueueLoop() {
|
|
6815
|
+
while (true) {
|
|
6816
|
+
const next = this.queue.shift();
|
|
6817
|
+
if (!next) {
|
|
6818
|
+
break;
|
|
6819
|
+
}
|
|
6820
|
+
const nextRid = next.command.requestId ?? `${next.source}-${Date.now()}`;
|
|
6821
|
+
await this.runSingleTurn(next.command, nextRid);
|
|
6822
|
+
}
|
|
6823
|
+
}
|
|
6824
|
+
/**
|
|
6825
|
+
* Resume draining the queue when the agent is idle. Acquires the lock,
|
|
6826
|
+
* drains, releases. Used by the `resume` stdin action (sandbox-initiated)
|
|
6827
|
+
* and by kickDrain (background-completion-initiated).
|
|
6828
|
+
*/
|
|
6829
|
+
async resumeQueue() {
|
|
6830
|
+
if (this.running || this.queue.length === 0) {
|
|
6831
|
+
return;
|
|
6832
|
+
}
|
|
6833
|
+
this.running = true;
|
|
6834
|
+
try {
|
|
6835
|
+
await this.drainQueueLoop();
|
|
6836
|
+
} finally {
|
|
6837
|
+
this.currentAbort = null;
|
|
6838
|
+
this.currentRequestId = void 0;
|
|
6839
|
+
this.running = false;
|
|
6840
|
+
}
|
|
6841
|
+
}
|
|
6842
|
+
/**
|
|
6843
|
+
* Kick off drainage of the queue when the agent is idle. Used by
|
|
6844
|
+
* onBackgroundComplete (when !running) to deliver results without
|
|
6845
|
+
* racing any currently-synchronous path.
|
|
6846
|
+
*/
|
|
6847
|
+
kickDrain() {
|
|
6848
|
+
if (this.running || this.queue.length === 0) {
|
|
6849
|
+
return;
|
|
6850
|
+
}
|
|
6851
|
+
setTimeout(() => this.resumeQueue(), 0);
|
|
6852
|
+
}
|
|
6853
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6854
|
+
// Simple command handlers
|
|
6855
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6856
|
+
handleClear() {
|
|
6857
|
+
clearSession(this.state);
|
|
6858
|
+
return {};
|
|
6859
|
+
}
|
|
6860
|
+
/** Cancel the running turn and drain the queue. Returns the drained items. */
|
|
6861
|
+
handleCancel() {
|
|
6862
|
+
if (this.currentAbort) {
|
|
6863
|
+
this.currentAbort.abort();
|
|
6864
|
+
}
|
|
6865
|
+
for (const [id, pending] of this.pendingTools) {
|
|
6866
|
+
clearTimeout(pending.timeout);
|
|
6867
|
+
pending.resolve("Error: cancelled");
|
|
6868
|
+
this.pendingTools.delete(id);
|
|
6617
6869
|
}
|
|
6618
|
-
|
|
6619
|
-
currentRequestId = void 0;
|
|
6620
|
-
running = false;
|
|
6870
|
+
return this.queue.drain();
|
|
6621
6871
|
}
|
|
6622
|
-
|
|
6623
|
-
|
|
6872
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6873
|
+
// Stdin router
|
|
6874
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
6875
|
+
handleStdinLine = async (line) => {
|
|
6624
6876
|
let parsed;
|
|
6625
6877
|
try {
|
|
6626
6878
|
parsed = JSON.parse(line);
|
|
6627
6879
|
} catch {
|
|
6628
|
-
emit("error", { error: "Invalid JSON on stdin" });
|
|
6880
|
+
this.emit("error", { error: "Invalid JSON on stdin" });
|
|
6629
6881
|
return;
|
|
6630
6882
|
}
|
|
6631
6883
|
const { action, requestId } = parsed;
|
|
6632
|
-
|
|
6884
|
+
log12.info("Command received", { action, requestId });
|
|
6633
6885
|
if (action === "tool_result" && parsed.id) {
|
|
6634
6886
|
const id = parsed.id;
|
|
6635
6887
|
const result = parsed.result ?? "";
|
|
6636
|
-
const pending = pendingTools.get(id);
|
|
6888
|
+
const pending = this.pendingTools.get(id);
|
|
6637
6889
|
if (pending) {
|
|
6638
|
-
pendingTools.delete(id);
|
|
6890
|
+
this.pendingTools.delete(id);
|
|
6639
6891
|
pending.resolve(result);
|
|
6640
|
-
} else if (!running) {
|
|
6641
|
-
|
|
6642
|
-
emit("completed", { success: true }, requestId);
|
|
6892
|
+
} else if (!this.running) {
|
|
6893
|
+
log12.info("Late tool_result while idle, dismissing", { id });
|
|
6894
|
+
this.emit("completed", { success: true }, requestId);
|
|
6643
6895
|
} else {
|
|
6644
|
-
earlyResults.set(id, result);
|
|
6896
|
+
this.earlyResults.set(id, result);
|
|
6645
6897
|
}
|
|
6646
6898
|
return;
|
|
6647
6899
|
}
|
|
6648
6900
|
if (action === "get_history") {
|
|
6649
|
-
applyPendingBlockUpdates();
|
|
6650
|
-
dispatchSimple(requestId, "history", () => ({
|
|
6651
|
-
messages: state.messages,
|
|
6652
|
-
running,
|
|
6653
|
-
...running && currentRequestId ? { currentRequestId } : {}
|
|
6901
|
+
this.applyPendingBlockUpdates();
|
|
6902
|
+
this.dispatchSimple(requestId, "history", () => ({
|
|
6903
|
+
messages: this.state.messages,
|
|
6904
|
+
running: this.running,
|
|
6905
|
+
...this.running && this.currentRequestId ? { currentRequestId: this.currentRequestId } : {},
|
|
6906
|
+
...this.queueFields()
|
|
6654
6907
|
}));
|
|
6655
6908
|
return;
|
|
6656
6909
|
}
|
|
6657
6910
|
if (action === "clear") {
|
|
6658
|
-
dispatchSimple(
|
|
6911
|
+
this.dispatchSimple(
|
|
6912
|
+
requestId,
|
|
6913
|
+
"session_cleared",
|
|
6914
|
+
() => this.handleClear()
|
|
6915
|
+
);
|
|
6659
6916
|
return;
|
|
6660
6917
|
}
|
|
6661
6918
|
if (action === "cancel") {
|
|
6662
|
-
handleCancel(
|
|
6663
|
-
emit(
|
|
6919
|
+
const cancelled = this.handleCancel();
|
|
6920
|
+
this.emit(
|
|
6921
|
+
"completed",
|
|
6922
|
+
{
|
|
6923
|
+
success: true,
|
|
6924
|
+
...cancelled.length > 0 && { cancelledMessages: cancelled }
|
|
6925
|
+
},
|
|
6926
|
+
requestId
|
|
6927
|
+
);
|
|
6664
6928
|
return;
|
|
6665
6929
|
}
|
|
6666
6930
|
if (action === "stop_tool") {
|
|
6667
6931
|
const id = parsed.id;
|
|
6668
6932
|
const mode = parsed.mode ?? "hard";
|
|
6669
|
-
const found = toolRegistry.stop(id, mode);
|
|
6933
|
+
const found = this.toolRegistry.stop(id, mode);
|
|
6670
6934
|
if (found) {
|
|
6671
|
-
emit("completed", { success: true }, requestId);
|
|
6935
|
+
this.emit("completed", { success: true }, requestId);
|
|
6672
6936
|
} else {
|
|
6673
|
-
emit(
|
|
6937
|
+
this.emit(
|
|
6674
6938
|
"completed",
|
|
6675
6939
|
{ success: false, error: "Tool not found" },
|
|
6676
6940
|
requestId
|
|
@@ -6681,11 +6945,11 @@ ${userMessage}` : header;
|
|
|
6681
6945
|
if (action === "restart_tool") {
|
|
6682
6946
|
const id = parsed.id;
|
|
6683
6947
|
const patchedInput = parsed.input;
|
|
6684
|
-
const found = toolRegistry.restart(id, patchedInput);
|
|
6948
|
+
const found = this.toolRegistry.restart(id, patchedInput);
|
|
6685
6949
|
if (found) {
|
|
6686
|
-
emit("completed", { success: true }, requestId);
|
|
6950
|
+
this.emit("completed", { success: true }, requestId);
|
|
6687
6951
|
} else {
|
|
6688
|
-
emit(
|
|
6952
|
+
this.emit(
|
|
6689
6953
|
"completed",
|
|
6690
6954
|
{ success: false, error: "Tool not found" },
|
|
6691
6955
|
requestId
|
|
@@ -6694,64 +6958,60 @@ ${userMessage}` : header;
|
|
|
6694
6958
|
return;
|
|
6695
6959
|
}
|
|
6696
6960
|
if (action === "compact") {
|
|
6697
|
-
triggerCompaction(state, config, {
|
|
6961
|
+
triggerCompaction(this.state, this.config, {
|
|
6698
6962
|
onStart: () => {
|
|
6699
|
-
sessionStats.compactionInProgress = true;
|
|
6700
|
-
|
|
6701
|
-
try {
|
|
6702
|
-
writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
|
|
6703
|
-
} catch {
|
|
6704
|
-
}
|
|
6963
|
+
this.sessionStats.compactionInProgress = true;
|
|
6964
|
+
this.persistStats();
|
|
6705
6965
|
},
|
|
6706
6966
|
onSummariesReady: () => {
|
|
6707
|
-
if (!running) {
|
|
6708
|
-
applyPendingSummaries();
|
|
6967
|
+
if (!this.running) {
|
|
6968
|
+
this.applyPendingSummaries();
|
|
6709
6969
|
}
|
|
6710
|
-
emit("compaction_complete", {}, requestId);
|
|
6711
|
-
emit("completed", { success: true }, requestId);
|
|
6970
|
+
this.emit("compaction_complete", {}, requestId);
|
|
6971
|
+
this.emit("completed", { success: true }, requestId);
|
|
6712
6972
|
},
|
|
6713
6973
|
onError: (error) => {
|
|
6714
|
-
emit("compaction_complete", { error }, requestId);
|
|
6715
|
-
emit("completed", { success: false, error }, requestId);
|
|
6974
|
+
this.emit("compaction_complete", { error }, requestId);
|
|
6975
|
+
this.emit("completed", { success: false, error }, requestId);
|
|
6716
6976
|
},
|
|
6717
6977
|
onFinally: () => {
|
|
6718
|
-
sessionStats.compactionInProgress = false;
|
|
6719
|
-
sessionStats.lastContextSize = 0;
|
|
6720
|
-
sessionStats.messageCount = state.messages.length;
|
|
6721
|
-
|
|
6722
|
-
try {
|
|
6723
|
-
writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
|
|
6724
|
-
} catch {
|
|
6725
|
-
}
|
|
6978
|
+
this.sessionStats.compactionInProgress = false;
|
|
6979
|
+
this.sessionStats.lastContextSize = 0;
|
|
6980
|
+
this.sessionStats.messageCount = this.state.messages.length;
|
|
6981
|
+
this.persistStats();
|
|
6726
6982
|
}
|
|
6727
6983
|
});
|
|
6728
6984
|
return;
|
|
6729
6985
|
}
|
|
6730
6986
|
if (action === "message") {
|
|
6731
|
-
await handleMessage(parsed, requestId);
|
|
6987
|
+
await this.handleMessage(parsed, requestId);
|
|
6732
6988
|
return;
|
|
6733
6989
|
}
|
|
6734
|
-
|
|
6735
|
-
|
|
6990
|
+
if (action === "resume") {
|
|
6991
|
+
if (this.running) {
|
|
6992
|
+
this.emit(
|
|
6993
|
+
"completed",
|
|
6994
|
+
{ success: false, error: "already running" },
|
|
6995
|
+
requestId
|
|
6996
|
+
);
|
|
6997
|
+
return;
|
|
6998
|
+
}
|
|
6999
|
+
if (this.queue.length === 0) {
|
|
7000
|
+
this.emit("completed", { success: true }, requestId);
|
|
7001
|
+
return;
|
|
7002
|
+
}
|
|
7003
|
+
this.emit("completed", { success: true }, requestId);
|
|
7004
|
+
await this.resumeQueue();
|
|
7005
|
+
return;
|
|
7006
|
+
}
|
|
7007
|
+
this.emit("error", { error: `Unknown action: ${action}` }, requestId);
|
|
7008
|
+
this.emit(
|
|
6736
7009
|
"completed",
|
|
6737
7010
|
{ success: false, error: `Unknown action: ${action}` },
|
|
6738
7011
|
requestId
|
|
6739
7012
|
);
|
|
6740
|
-
}
|
|
6741
|
-
|
|
6742
|
-
emit("stopping");
|
|
6743
|
-
emit("stopped");
|
|
6744
|
-
process.exit(0);
|
|
6745
|
-
});
|
|
6746
|
-
function shutdown() {
|
|
6747
|
-
emit("stopping");
|
|
6748
|
-
emit("stopped");
|
|
6749
|
-
process.exit(0);
|
|
6750
|
-
}
|
|
6751
|
-
process.on("SIGTERM", shutdown);
|
|
6752
|
-
process.on("SIGINT", shutdown);
|
|
6753
|
-
emit("ready");
|
|
6754
|
-
}
|
|
7013
|
+
};
|
|
7014
|
+
};
|
|
6755
7015
|
export {
|
|
6756
|
-
|
|
7016
|
+
HeadlessSession
|
|
6757
7017
|
};
|