@mindstudio-ai/remy 0.1.147 → 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 +747 -486
- package/dist/index.js +1068 -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/index.js
CHANGED
|
@@ -830,7 +830,7 @@ var init_writePlan = __esm({
|
|
|
830
830
|
clearable: false,
|
|
831
831
|
definition: {
|
|
832
832
|
name: "writePlan",
|
|
833
|
-
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
|
|
833
|
+
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.",
|
|
834
834
|
inputSchema: {
|
|
835
835
|
type: "object",
|
|
836
836
|
properties: {
|
|
@@ -912,13 +912,13 @@ var init_setProjectOnboardingState = __esm({
|
|
|
912
912
|
clearable: false,
|
|
913
913
|
definition: {
|
|
914
914
|
name: "setProjectOnboardingState",
|
|
915
|
-
description: "Advance the project onboarding state.
|
|
915
|
+
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.",
|
|
916
916
|
inputSchema: {
|
|
917
917
|
type: "object",
|
|
918
918
|
properties: {
|
|
919
919
|
state: {
|
|
920
920
|
type: "string",
|
|
921
|
-
enum: ["
|
|
921
|
+
enum: ["building", "buildComplete", "onboardingFinished"],
|
|
922
922
|
description: "The onboarding state to advance to."
|
|
923
923
|
}
|
|
924
924
|
},
|
|
@@ -1793,11 +1793,11 @@ Old tool results are periodically cleared from the conversation to save context
|
|
|
1793
1793
|
</conversation_summaries>
|
|
1794
1794
|
|
|
1795
1795
|
<project_onboarding>
|
|
1796
|
-
New projects progress through
|
|
1796
|
+
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:
|
|
1797
1797
|
|
|
1798
|
-
- **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.
|
|
1799
|
-
- **
|
|
1800
|
-
- **
|
|
1798
|
+
- **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.
|
|
1799
|
+
- **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).
|
|
1800
|
+
- **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.
|
|
1801
1801
|
- **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.
|
|
1802
1802
|
</project_onboarding>
|
|
1803
1803
|
|
|
@@ -1872,7 +1872,7 @@ var init_compactConversation = __esm({
|
|
|
1872
1872
|
clearable: false,
|
|
1873
1873
|
definition: {
|
|
1874
1874
|
name: "compactConversation",
|
|
1875
|
-
description: "Compact the conversation history by summarizing older messages into a checkpoint. The summary preserves key decisions, what was built, and the current state of the project, but drops the verbose tool results, diffs, and intermediate steps that are no longer useful.
|
|
1875
|
+
description: "Compact the conversation history by summarizing older messages into a checkpoint. The summary preserves key decisions, what was built, and the current state of the project, but drops the verbose tool results, diffs, and intermediate steps that are no longer useful. Runs in the background.",
|
|
1876
1876
|
inputSchema: {
|
|
1877
1877
|
type: "object",
|
|
1878
1878
|
properties: {}
|
|
@@ -2788,7 +2788,7 @@ var init_runMethod = __esm({
|
|
|
2788
2788
|
clearable: true,
|
|
2789
2789
|
definition: {
|
|
2790
2790
|
name: "runMethod",
|
|
2791
|
-
description:
|
|
2791
|
+
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.',
|
|
2792
2792
|
inputSchema: {
|
|
2793
2793
|
type: "object",
|
|
2794
2794
|
properties: {
|
|
@@ -2800,14 +2800,14 @@ var init_runMethod = __esm({
|
|
|
2800
2800
|
type: "object",
|
|
2801
2801
|
description: "The input payload to pass to the method. Omit for methods that take no input."
|
|
2802
2802
|
},
|
|
2803
|
+
userId: {
|
|
2804
|
+
type: "string",
|
|
2805
|
+
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.'
|
|
2806
|
+
},
|
|
2803
2807
|
roles: {
|
|
2804
2808
|
type: "array",
|
|
2805
2809
|
items: { type: "string" },
|
|
2806
|
-
description: 'Optional. Role names for this request (e.g. ["admin"]).
|
|
2807
|
-
},
|
|
2808
|
-
userId: {
|
|
2809
|
-
type: "string",
|
|
2810
|
-
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."
|
|
2810
|
+
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.'
|
|
2811
2811
|
}
|
|
2812
2812
|
},
|
|
2813
2813
|
required: ["method"]
|
|
@@ -2937,10 +2937,10 @@ var init_screenshot = __esm({
|
|
|
2937
2937
|
"use strict";
|
|
2938
2938
|
init_sidecar();
|
|
2939
2939
|
init_analyzeImage();
|
|
2940
|
-
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)
|
|
2940
|
+
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).`;
|
|
2941
|
+
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.
|
|
2941
2942
|
|
|
2942
2943
|
Respond only with your analysis as Markdown and absolutely no other text. Do not use emojis - use unicode if you need symbols.`;
|
|
2943
|
-
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.`;
|
|
2944
2944
|
}
|
|
2945
2945
|
});
|
|
2946
2946
|
|
|
@@ -2964,31 +2964,30 @@ async function checkBrowserConnected() {
|
|
|
2964
2964
|
if (!status.connected) {
|
|
2965
2965
|
return {
|
|
2966
2966
|
connected: false,
|
|
2967
|
-
|
|
2967
|
+
reason: BROWSER_UNAVAILABLE_MESSAGE
|
|
2968
2968
|
};
|
|
2969
2969
|
}
|
|
2970
2970
|
return { connected: true };
|
|
2971
|
-
} catch
|
|
2971
|
+
} catch {
|
|
2972
2972
|
return {
|
|
2973
2973
|
connected: false,
|
|
2974
|
-
|
|
2974
|
+
reason: BROWSER_UNAVAILABLE_MESSAGE
|
|
2975
2975
|
};
|
|
2976
2976
|
}
|
|
2977
2977
|
}
|
|
2978
|
-
var lockQueue;
|
|
2978
|
+
var lockQueue, BROWSER_UNAVAILABLE_MESSAGE;
|
|
2979
2979
|
var init_browserLock = __esm({
|
|
2980
2980
|
"src/tools/_helpers/browserLock.ts"() {
|
|
2981
2981
|
"use strict";
|
|
2982
2982
|
init_sidecar();
|
|
2983
2983
|
lockQueue = Promise.resolve();
|
|
2984
|
+
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.";
|
|
2984
2985
|
}
|
|
2985
2986
|
});
|
|
2986
2987
|
|
|
2987
2988
|
// src/statusWatcher.ts
|
|
2988
2989
|
function startStatusWatcher(config) {
|
|
2989
|
-
const { apiConfig, getContext, onStatus, interval =
|
|
2990
|
-
let lastLabel = "";
|
|
2991
|
-
let lastContext = "";
|
|
2990
|
+
const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
|
|
2992
2991
|
let inflight = false;
|
|
2993
2992
|
let stopped = false;
|
|
2994
2993
|
const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
|
|
@@ -2999,10 +2998,9 @@ function startStatusWatcher(config) {
|
|
|
2999
2998
|
inflight = true;
|
|
3000
2999
|
try {
|
|
3001
3000
|
const context = getContext();
|
|
3002
|
-
if (!context
|
|
3001
|
+
if (!context) {
|
|
3003
3002
|
return;
|
|
3004
3003
|
}
|
|
3005
|
-
lastContext = context;
|
|
3006
3004
|
const res = await fetch(url, {
|
|
3007
3005
|
method: "POST",
|
|
3008
3006
|
headers: {
|
|
@@ -3012,15 +3010,11 @@ function startStatusWatcher(config) {
|
|
|
3012
3010
|
body: JSON.stringify({ context }),
|
|
3013
3011
|
signal
|
|
3014
3012
|
});
|
|
3015
|
-
if (!res.ok) {
|
|
3013
|
+
if (!res.ok || stopped) {
|
|
3016
3014
|
return;
|
|
3017
3015
|
}
|
|
3018
3016
|
const data = await res.json();
|
|
3019
|
-
if (!data.label
|
|
3020
|
-
return;
|
|
3021
|
-
}
|
|
3022
|
-
lastLabel = data.label;
|
|
3023
|
-
if (stopped) {
|
|
3017
|
+
if (!data.label) {
|
|
3024
3018
|
return;
|
|
3025
3019
|
}
|
|
3026
3020
|
onStatus(data.label);
|
|
@@ -3045,6 +3039,48 @@ var init_statusWatcher = __esm({
|
|
|
3045
3039
|
}
|
|
3046
3040
|
});
|
|
3047
3041
|
|
|
3042
|
+
// src/automatedActions/sentinel.ts
|
|
3043
|
+
function sentinel(name) {
|
|
3044
|
+
return `@@automated::${name}@@`;
|
|
3045
|
+
}
|
|
3046
|
+
function automatedMessage(name, body) {
|
|
3047
|
+
return body ? `${sentinel(name)}
|
|
3048
|
+
${body}` : sentinel(name);
|
|
3049
|
+
}
|
|
3050
|
+
function hasSentinel(text, name) {
|
|
3051
|
+
return text.startsWith(sentinel(name));
|
|
3052
|
+
}
|
|
3053
|
+
function isAutomatedMessage(text) {
|
|
3054
|
+
return text.startsWith("@@automated::");
|
|
3055
|
+
}
|
|
3056
|
+
function parseSentinel(text) {
|
|
3057
|
+
const match = text.match(/^@@automated::(\w+)@@(.*)/s);
|
|
3058
|
+
if (!match) {
|
|
3059
|
+
return null;
|
|
3060
|
+
}
|
|
3061
|
+
return { name: match[1], remainder: match[2] };
|
|
3062
|
+
}
|
|
3063
|
+
function stripSentinelLine(text) {
|
|
3064
|
+
return text.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "");
|
|
3065
|
+
}
|
|
3066
|
+
function buildBackgroundResultsMessage(results) {
|
|
3067
|
+
const xml = results.map(
|
|
3068
|
+
(r) => `<tool_result id="${r.toolCallId}" name="${r.name}">
|
|
3069
|
+
${r.result}
|
|
3070
|
+
</tool_result>`
|
|
3071
|
+
).join("\n\n");
|
|
3072
|
+
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.
|
|
3073
|
+
<background_results>
|
|
3074
|
+
${xml}
|
|
3075
|
+
</background_results>`;
|
|
3076
|
+
return automatedMessage("background_results", body);
|
|
3077
|
+
}
|
|
3078
|
+
var init_sentinel = __esm({
|
|
3079
|
+
"src/automatedActions/sentinel.ts"() {
|
|
3080
|
+
"use strict";
|
|
3081
|
+
}
|
|
3082
|
+
});
|
|
3083
|
+
|
|
3048
3084
|
// src/subagents/common/cleanMessages.ts
|
|
3049
3085
|
function findLastSummaryCheckpoint(messages, name) {
|
|
3050
3086
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
@@ -3138,11 +3174,8 @@ ${summaryBlock.text}
|
|
|
3138
3174
|
}
|
|
3139
3175
|
return true;
|
|
3140
3176
|
}).map((msg) => {
|
|
3141
|
-
if (msg.role === "user" && typeof msg.content === "string" && msg.content
|
|
3142
|
-
return {
|
|
3143
|
-
...msg,
|
|
3144
|
-
content: msg.content.replace(/^@@automated::[^@]*@@[^\n]*\n?/, "")
|
|
3145
|
-
};
|
|
3177
|
+
if (msg.role === "user" && typeof msg.content === "string" && isAutomatedMessage(msg.content)) {
|
|
3178
|
+
return { ...msg, content: stripSentinelLine(msg.content) };
|
|
3146
3179
|
}
|
|
3147
3180
|
if (!Array.isArray(msg.content)) {
|
|
3148
3181
|
return msg;
|
|
@@ -3173,6 +3206,7 @@ ${summaryBlock.text}
|
|
|
3173
3206
|
var init_cleanMessages = __esm({
|
|
3174
3207
|
"src/subagents/common/cleanMessages.ts"() {
|
|
3175
3208
|
"use strict";
|
|
3209
|
+
init_sentinel();
|
|
3176
3210
|
}
|
|
3177
3211
|
});
|
|
3178
3212
|
|
|
@@ -3204,7 +3238,7 @@ async function runSubAgent(config) {
|
|
|
3204
3238
|
const agentName = subAgentId || "sub-agent";
|
|
3205
3239
|
const runStart = Date.now();
|
|
3206
3240
|
log5.info("Sub-agent started", { requestId, parentToolId, agentName });
|
|
3207
|
-
const
|
|
3241
|
+
const emit = (e) => {
|
|
3208
3242
|
onEvent({ ...e, parentToolId });
|
|
3209
3243
|
};
|
|
3210
3244
|
const dateStr = (/* @__PURE__ */ new Date()).toLocaleDateString("en-US", {
|
|
@@ -3269,7 +3303,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3269
3303
|
}
|
|
3270
3304
|
return parts.join("\n");
|
|
3271
3305
|
},
|
|
3272
|
-
onStatus: (label) =>
|
|
3306
|
+
onStatus: (label) => emit({ type: "status", message: label }),
|
|
3273
3307
|
signal
|
|
3274
3308
|
});
|
|
3275
3309
|
try {
|
|
@@ -3286,7 +3320,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3286
3320
|
signal
|
|
3287
3321
|
},
|
|
3288
3322
|
{
|
|
3289
|
-
onRetry: (attempt) =>
|
|
3323
|
+
onRetry: (attempt) => emit({
|
|
3290
3324
|
type: "status",
|
|
3291
3325
|
message: `Lost connection, retrying (attempt ${attempt + 2} of 3)...`
|
|
3292
3326
|
})
|
|
@@ -3307,14 +3341,14 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3307
3341
|
startedAt: event.ts
|
|
3308
3342
|
});
|
|
3309
3343
|
}
|
|
3310
|
-
|
|
3344
|
+
emit({ type: "text", text: event.text });
|
|
3311
3345
|
break;
|
|
3312
3346
|
}
|
|
3313
3347
|
case "thinking":
|
|
3314
3348
|
if (!thinkingStartedAt) {
|
|
3315
3349
|
thinkingStartedAt = event.ts;
|
|
3316
3350
|
}
|
|
3317
|
-
|
|
3351
|
+
emit({ type: "thinking", text: event.text });
|
|
3318
3352
|
break;
|
|
3319
3353
|
case "thinking_complete":
|
|
3320
3354
|
contentBlocks.push({
|
|
@@ -3334,7 +3368,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3334
3368
|
input: event.input,
|
|
3335
3369
|
startedAt: Date.now()
|
|
3336
3370
|
});
|
|
3337
|
-
|
|
3371
|
+
emit({
|
|
3338
3372
|
type: "tool_start",
|
|
3339
3373
|
id: event.id,
|
|
3340
3374
|
name: event.name,
|
|
@@ -3411,7 +3445,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3411
3445
|
if (externalTools.has(tc.name) && resolveExternalTool) {
|
|
3412
3446
|
result = await resolveExternalTool(tc.id, tc.name, input);
|
|
3413
3447
|
} else {
|
|
3414
|
-
const onLog = (line) =>
|
|
3448
|
+
const onLog = (line) => emit({
|
|
3415
3449
|
type: "tool_input_delta",
|
|
3416
3450
|
id: tc.id,
|
|
3417
3451
|
name: tc.name,
|
|
@@ -3462,7 +3496,7 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
|
|
|
3462
3496
|
durationMs: Date.now() - toolStart,
|
|
3463
3497
|
isError: r.isError
|
|
3464
3498
|
});
|
|
3465
|
-
|
|
3499
|
+
emit({
|
|
3466
3500
|
type: "tool_done",
|
|
3467
3501
|
id: tc.id,
|
|
3468
3502
|
name: tc.name,
|
|
@@ -3767,7 +3801,7 @@ var init_browserAutomation = __esm({
|
|
|
3767
3801
|
try {
|
|
3768
3802
|
const browserStatus = await checkBrowserConnected();
|
|
3769
3803
|
if (!browserStatus.connected) {
|
|
3770
|
-
return
|
|
3804
|
+
return browserStatus.reason ?? "Browser preview unavailable.";
|
|
3771
3805
|
}
|
|
3772
3806
|
try {
|
|
3773
3807
|
await sidecarRequest("/reset-browser", {}, { timeout: 5e3 });
|
|
@@ -3963,7 +3997,7 @@ var init_screenshot2 = __esm({
|
|
|
3963
3997
|
try {
|
|
3964
3998
|
const browserStatus = await checkBrowserConnected();
|
|
3965
3999
|
if (!browserStatus.connected) {
|
|
3966
|
-
return
|
|
4000
|
+
return browserStatus.reason ?? "Browser preview unavailable.";
|
|
3967
4001
|
}
|
|
3968
4002
|
return await captureAndAnalyzeScreenshot({
|
|
3969
4003
|
prompt: input.prompt,
|
|
@@ -4317,7 +4351,7 @@ async function execute5(input, onLog, context) {
|
|
|
4317
4351
|
try {
|
|
4318
4352
|
const browserStatus = await checkBrowserConnected();
|
|
4319
4353
|
if (!browserStatus.connected) {
|
|
4320
|
-
return
|
|
4354
|
+
return browserStatus.reason ?? "Browser preview unavailable.";
|
|
4321
4355
|
}
|
|
4322
4356
|
return await captureAndAnalyzeScreenshot({
|
|
4323
4357
|
prompt: input.prompt,
|
|
@@ -4826,7 +4860,7 @@ Each interface type invokes the same backend methods. Methods don't know which i
|
|
|
4826
4860
|
TypeScript running in a sandboxed environment. Any npm package can be installed. Key capabilities:
|
|
4827
4861
|
|
|
4828
4862
|
- Managed SQLite database with typed schemas and automatic migrations. Define a TypeScript interface, push, and the platform handles diffing and migrating.
|
|
4829
|
-
- Built-in app-managed auth. Opt-in via manifest \u2014 developer builds login UI, platform handles verification codes (email
|
|
4863
|
+
- 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.
|
|
4830
4864
|
- Encrypted secrets with separate dev/prod values, injected as process.env. For third-party service credentials not covered by the SDK.
|
|
4831
4865
|
- Git-native deployment. Push to default branch to deploy.
|
|
4832
4866
|
|
|
@@ -5324,7 +5358,7 @@ Use <current_deck> as your starting point and replace or update the content as n
|
|
|
5324
5358
|
- The deck must be a single HTML file \u2014 it will be rendered in an iframe.
|
|
5325
5359
|
- Must look beautiful on desktop and mobile.
|
|
5326
5360
|
- 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.
|
|
5327
|
-
- Be bold and impactful.
|
|
5361
|
+
- Be bold and impactful. Use images from the spec or generate new images when needed.
|
|
5328
5362
|
- 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.
|
|
5329
5363
|
- Keep the progress bar and edge chevrons from the shell \u2014 they are part of the navigation UX.
|
|
5330
5364
|
|
|
@@ -6108,20 +6142,23 @@ async function runTurn(params) {
|
|
|
6108
6142
|
parts.push(`Tool: ${toolName}`);
|
|
6109
6143
|
}
|
|
6110
6144
|
if (lastCompletedInput) {
|
|
6111
|
-
parts.push(`Tool input: ${lastCompletedInput.slice(-
|
|
6145
|
+
parts.push(`Tool input: ${lastCompletedInput.slice(-1500)}`);
|
|
6112
6146
|
}
|
|
6113
6147
|
if (lastCompletedResult) {
|
|
6114
|
-
parts.push(`Tool result: ${lastCompletedResult.slice(-
|
|
6148
|
+
parts.push(`Tool result: ${lastCompletedResult.slice(-1500)}`);
|
|
6115
6149
|
}
|
|
6116
|
-
const text = subAgentText || getTextContent(contentBlocks).slice(-
|
|
6150
|
+
const text = subAgentText || getTextContent(contentBlocks).slice(-2e3);
|
|
6117
6151
|
if (text) {
|
|
6118
6152
|
parts.push(`Assistant text: ${text}`);
|
|
6119
6153
|
}
|
|
6120
6154
|
if (onboardingState && onboardingState !== "onboardingFinished") {
|
|
6121
6155
|
parts.push(`Build phase: ${onboardingState}`);
|
|
6122
6156
|
}
|
|
6123
|
-
|
|
6124
|
-
|
|
6157
|
+
const automated = parseSentinel(userMessage);
|
|
6158
|
+
if (automated) {
|
|
6159
|
+
parts.push(`Automated action: ${automated.name}`);
|
|
6160
|
+
} else if (userMessage) {
|
|
6161
|
+
parts.push(`User request: ${userMessage.slice(-500)}`);
|
|
6125
6162
|
}
|
|
6126
6163
|
return parts.join("\n");
|
|
6127
6164
|
},
|
|
@@ -6518,6 +6555,7 @@ var init_agent = __esm({
|
|
|
6518
6555
|
init_errors();
|
|
6519
6556
|
init_cleanMessages();
|
|
6520
6557
|
init_tools6();
|
|
6558
|
+
init_sentinel();
|
|
6521
6559
|
log8 = createLogger("agent");
|
|
6522
6560
|
EXTERNAL_TOOLS = /* @__PURE__ */ new Set([
|
|
6523
6561
|
"promptUser",
|
|
@@ -6667,18 +6705,267 @@ ${partial}` : "[INTERRUPTED] Tool execution was stopped.";
|
|
|
6667
6705
|
}
|
|
6668
6706
|
});
|
|
6669
6707
|
|
|
6708
|
+
// src/headless/attachments.ts
|
|
6709
|
+
import { mkdirSync, existsSync } from "fs";
|
|
6710
|
+
import { writeFile } from "fs/promises";
|
|
6711
|
+
import { basename, join, extname } from "path";
|
|
6712
|
+
function filenameFromUrl(url) {
|
|
6713
|
+
try {
|
|
6714
|
+
const pathname = new URL(url).pathname;
|
|
6715
|
+
const name = basename(pathname);
|
|
6716
|
+
return name && name !== "/" ? decodeURIComponent(name) : `upload-${Date.now()}`;
|
|
6717
|
+
} catch {
|
|
6718
|
+
return `upload-${Date.now()}`;
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
function resolveUniqueFilename(name) {
|
|
6722
|
+
if (!existsSync(join(UPLOADS_DIR, name))) {
|
|
6723
|
+
return name;
|
|
6724
|
+
}
|
|
6725
|
+
const ext = extname(name);
|
|
6726
|
+
const base = name.slice(0, name.length - ext.length);
|
|
6727
|
+
let counter = 1;
|
|
6728
|
+
while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
|
|
6729
|
+
counter++;
|
|
6730
|
+
}
|
|
6731
|
+
return `${base}-${counter}${ext}`;
|
|
6732
|
+
}
|
|
6733
|
+
function isImageAttachment(att) {
|
|
6734
|
+
const name = att.filename || filenameFromUrl(att.url);
|
|
6735
|
+
return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
|
|
6736
|
+
}
|
|
6737
|
+
async function persistAttachments(attachments) {
|
|
6738
|
+
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
6739
|
+
if (nonVoice.length === 0) {
|
|
6740
|
+
return { documents: [], images: [] };
|
|
6741
|
+
}
|
|
6742
|
+
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
6743
|
+
const results = await Promise.allSettled(
|
|
6744
|
+
nonVoice.map(async (att) => {
|
|
6745
|
+
const name = resolveUniqueFilename(
|
|
6746
|
+
att.filename || filenameFromUrl(att.url)
|
|
6747
|
+
);
|
|
6748
|
+
const localPath = join(UPLOADS_DIR, name);
|
|
6749
|
+
const res = await fetch(att.url, {
|
|
6750
|
+
signal: AbortSignal.timeout(3e4)
|
|
6751
|
+
});
|
|
6752
|
+
if (!res.ok) {
|
|
6753
|
+
throw new Error(`HTTP ${res.status} downloading ${att.url}`);
|
|
6754
|
+
}
|
|
6755
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
6756
|
+
await writeFile(localPath, buffer);
|
|
6757
|
+
log11.info("Attachment saved", {
|
|
6758
|
+
filename: name,
|
|
6759
|
+
path: localPath,
|
|
6760
|
+
bytes: buffer.length
|
|
6761
|
+
});
|
|
6762
|
+
let extractedTextPath;
|
|
6763
|
+
if (att.extractedTextUrl) {
|
|
6764
|
+
try {
|
|
6765
|
+
const textRes = await fetch(att.extractedTextUrl, {
|
|
6766
|
+
signal: AbortSignal.timeout(3e4)
|
|
6767
|
+
});
|
|
6768
|
+
if (textRes.ok) {
|
|
6769
|
+
extractedTextPath = `${localPath}.txt`;
|
|
6770
|
+
await writeFile(extractedTextPath, await textRes.text(), "utf-8");
|
|
6771
|
+
log11.info("Extracted text saved", { path: extractedTextPath });
|
|
6772
|
+
}
|
|
6773
|
+
} catch {
|
|
6774
|
+
}
|
|
6775
|
+
}
|
|
6776
|
+
return {
|
|
6777
|
+
filename: name,
|
|
6778
|
+
localPath,
|
|
6779
|
+
remoteUrl: att.url,
|
|
6780
|
+
extractedTextPath
|
|
6781
|
+
};
|
|
6782
|
+
})
|
|
6783
|
+
);
|
|
6784
|
+
const settled = results.map((r, i) => ({
|
|
6785
|
+
result: r.status === "fulfilled" ? r.value : null,
|
|
6786
|
+
isImage: isImageAttachment(nonVoice[i])
|
|
6787
|
+
}));
|
|
6788
|
+
return {
|
|
6789
|
+
documents: settled.filter((s) => !s.isImage).map((s) => s.result),
|
|
6790
|
+
images: settled.filter((s) => s.isImage).map((s) => s.result)
|
|
6791
|
+
};
|
|
6792
|
+
}
|
|
6793
|
+
function buildUploadHeader(results) {
|
|
6794
|
+
const succeeded = results.filter(Boolean);
|
|
6795
|
+
if (succeeded.length === 0) {
|
|
6796
|
+
return "";
|
|
6797
|
+
}
|
|
6798
|
+
if (succeeded.length === 1) {
|
|
6799
|
+
const r = succeeded[0];
|
|
6800
|
+
const parts = [`[Uploaded file: ${r.localPath} (CDN: ${r.remoteUrl})`];
|
|
6801
|
+
if (r.extractedTextPath) {
|
|
6802
|
+
parts.push(`extracted text: ${r.extractedTextPath}`);
|
|
6803
|
+
}
|
|
6804
|
+
return parts.join(" \u2014 ") + "]";
|
|
6805
|
+
}
|
|
6806
|
+
const lines = succeeded.map((r) => {
|
|
6807
|
+
const parts = [`- ${r.localPath} (CDN: ${r.remoteUrl})`];
|
|
6808
|
+
if (r.extractedTextPath) {
|
|
6809
|
+
parts.push(` extracted text: ${r.extractedTextPath}`);
|
|
6810
|
+
}
|
|
6811
|
+
return parts.join("\n");
|
|
6812
|
+
});
|
|
6813
|
+
return `[Uploaded files]
|
|
6814
|
+
${lines.join("\n")}`;
|
|
6815
|
+
}
|
|
6816
|
+
var log11, UPLOADS_DIR, IMAGE_EXTENSIONS;
|
|
6817
|
+
var init_attachments = __esm({
|
|
6818
|
+
"src/headless/attachments.ts"() {
|
|
6819
|
+
"use strict";
|
|
6820
|
+
init_logger();
|
|
6821
|
+
log11 = createLogger("headless:attachments");
|
|
6822
|
+
UPLOADS_DIR = "src/.user-uploads";
|
|
6823
|
+
IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
6824
|
+
".png",
|
|
6825
|
+
".jpg",
|
|
6826
|
+
".jpeg",
|
|
6827
|
+
".gif",
|
|
6828
|
+
".webp",
|
|
6829
|
+
".svg",
|
|
6830
|
+
".bmp",
|
|
6831
|
+
".ico",
|
|
6832
|
+
".tiff",
|
|
6833
|
+
".tif",
|
|
6834
|
+
".avif",
|
|
6835
|
+
".heic",
|
|
6836
|
+
".heif"
|
|
6837
|
+
]);
|
|
6838
|
+
}
|
|
6839
|
+
});
|
|
6840
|
+
|
|
6841
|
+
// src/headless/planFile.ts
|
|
6842
|
+
import { readFileSync, writeFileSync, unlinkSync } from "fs";
|
|
6843
|
+
function applyPlanFileSideEffect(rawText) {
|
|
6844
|
+
if (hasSentinel(rawText, "approvePlan") || hasSentinel(rawText, "approveInitialPlan")) {
|
|
6845
|
+
try {
|
|
6846
|
+
const plan = readFileSync(PLAN_FILE3, "utf-8");
|
|
6847
|
+
writeFileSync(
|
|
6848
|
+
PLAN_FILE3,
|
|
6849
|
+
plan.replace(/^status:\s*pending/m, "status: approved"),
|
|
6850
|
+
"utf-8"
|
|
6851
|
+
);
|
|
6852
|
+
} catch {
|
|
6853
|
+
}
|
|
6854
|
+
} else if (hasSentinel(rawText, "rejectPlan")) {
|
|
6855
|
+
try {
|
|
6856
|
+
unlinkSync(PLAN_FILE3);
|
|
6857
|
+
} catch {
|
|
6858
|
+
}
|
|
6859
|
+
}
|
|
6860
|
+
}
|
|
6861
|
+
var PLAN_FILE3;
|
|
6862
|
+
var init_planFile = __esm({
|
|
6863
|
+
"src/headless/planFile.ts"() {
|
|
6864
|
+
"use strict";
|
|
6865
|
+
init_sentinel();
|
|
6866
|
+
PLAN_FILE3 = ".remy-plan.md";
|
|
6867
|
+
}
|
|
6868
|
+
});
|
|
6869
|
+
|
|
6870
|
+
// src/headless/stats.ts
|
|
6871
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6872
|
+
function createSessionStats() {
|
|
6873
|
+
return {
|
|
6874
|
+
messageCount: 0,
|
|
6875
|
+
turns: 0,
|
|
6876
|
+
totalInputTokens: 0,
|
|
6877
|
+
totalOutputTokens: 0,
|
|
6878
|
+
totalCacheCreationTokens: 0,
|
|
6879
|
+
totalCacheReadTokens: 0,
|
|
6880
|
+
lastContextSize: 0,
|
|
6881
|
+
compactionInProgress: false,
|
|
6882
|
+
updatedAt: 0
|
|
6883
|
+
};
|
|
6884
|
+
}
|
|
6885
|
+
function loadQueue() {
|
|
6886
|
+
try {
|
|
6887
|
+
const stats = JSON.parse(readFileSync2(STATS_FILE, "utf-8"));
|
|
6888
|
+
if (Array.isArray(stats.queue)) {
|
|
6889
|
+
return stats.queue;
|
|
6890
|
+
}
|
|
6891
|
+
} catch {
|
|
6892
|
+
}
|
|
6893
|
+
return [];
|
|
6894
|
+
}
|
|
6895
|
+
function writeStats(stats, queue) {
|
|
6896
|
+
try {
|
|
6897
|
+
writeFileSync2(
|
|
6898
|
+
STATS_FILE,
|
|
6899
|
+
JSON.stringify({
|
|
6900
|
+
...stats,
|
|
6901
|
+
queue
|
|
6902
|
+
})
|
|
6903
|
+
);
|
|
6904
|
+
} catch {
|
|
6905
|
+
}
|
|
6906
|
+
}
|
|
6907
|
+
var STATS_FILE;
|
|
6908
|
+
var init_stats = __esm({
|
|
6909
|
+
"src/headless/stats.ts"() {
|
|
6910
|
+
"use strict";
|
|
6911
|
+
STATS_FILE = ".remy-stats.json";
|
|
6912
|
+
}
|
|
6913
|
+
});
|
|
6914
|
+
|
|
6915
|
+
// src/headless/messageQueue.ts
|
|
6916
|
+
var MessageQueue;
|
|
6917
|
+
var init_messageQueue = __esm({
|
|
6918
|
+
"src/headless/messageQueue.ts"() {
|
|
6919
|
+
"use strict";
|
|
6920
|
+
MessageQueue = class {
|
|
6921
|
+
items = [];
|
|
6922
|
+
onChange;
|
|
6923
|
+
constructor(initial = [], onChange) {
|
|
6924
|
+
this.items = [...initial];
|
|
6925
|
+
this.onChange = onChange;
|
|
6926
|
+
}
|
|
6927
|
+
push(item) {
|
|
6928
|
+
this.items.push(item);
|
|
6929
|
+
this.onChange?.();
|
|
6930
|
+
}
|
|
6931
|
+
shift() {
|
|
6932
|
+
const item = this.items.shift();
|
|
6933
|
+
if (item) {
|
|
6934
|
+
this.onChange?.();
|
|
6935
|
+
}
|
|
6936
|
+
return item;
|
|
6937
|
+
}
|
|
6938
|
+
/** Remove and return all queued items. */
|
|
6939
|
+
drain() {
|
|
6940
|
+
if (this.items.length === 0) {
|
|
6941
|
+
return [];
|
|
6942
|
+
}
|
|
6943
|
+
const all = this.items.splice(0);
|
|
6944
|
+
this.onChange?.();
|
|
6945
|
+
return all;
|
|
6946
|
+
}
|
|
6947
|
+
/** Copy of current queue contents (for surfacing on events). */
|
|
6948
|
+
snapshot() {
|
|
6949
|
+
return [...this.items];
|
|
6950
|
+
}
|
|
6951
|
+
get length() {
|
|
6952
|
+
return this.items.length;
|
|
6953
|
+
}
|
|
6954
|
+
};
|
|
6955
|
+
}
|
|
6956
|
+
});
|
|
6957
|
+
|
|
6670
6958
|
// src/automatedActions/resolve.ts
|
|
6671
6959
|
function resolveAction(text) {
|
|
6672
|
-
const
|
|
6673
|
-
if (!
|
|
6960
|
+
const parsed = parseSentinel(text);
|
|
6961
|
+
if (!parsed) {
|
|
6674
6962
|
return null;
|
|
6675
6963
|
}
|
|
6676
|
-
const triggerName =
|
|
6964
|
+
const { name: triggerName, remainder } = parsed;
|
|
6677
6965
|
if (NON_ACTION_SENTINELS.has(triggerName)) {
|
|
6678
6966
|
return null;
|
|
6679
6967
|
}
|
|
6680
6968
|
let params = {};
|
|
6681
|
-
const remainder = match[2];
|
|
6682
6969
|
if (remainder) {
|
|
6683
6970
|
try {
|
|
6684
6971
|
params = JSON.parse(remainder.split("\n")[0]);
|
|
@@ -6700,8 +6987,7 @@ function resolveAction(text) {
|
|
|
6700
6987
|
body = body.replaceAll(`{{${key}}}`, str);
|
|
6701
6988
|
}
|
|
6702
6989
|
return {
|
|
6703
|
-
message:
|
|
6704
|
-
${body}`,
|
|
6990
|
+
message: automatedMessage(triggerName, body),
|
|
6705
6991
|
next
|
|
6706
6992
|
};
|
|
6707
6993
|
}
|
|
@@ -6710,744 +6996,758 @@ var init_resolve = __esm({
|
|
|
6710
6996
|
"src/automatedActions/resolve.ts"() {
|
|
6711
6997
|
"use strict";
|
|
6712
6998
|
init_assets();
|
|
6999
|
+
init_sentinel();
|
|
6713
7000
|
NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
|
|
6714
7001
|
}
|
|
6715
7002
|
});
|
|
6716
7003
|
|
|
6717
|
-
// src/headless.ts
|
|
7004
|
+
// src/headless/index.ts
|
|
6718
7005
|
var headless_exports = {};
|
|
6719
7006
|
__export(headless_exports, {
|
|
6720
|
-
|
|
7007
|
+
HeadlessSession: () => HeadlessSession
|
|
6721
7008
|
});
|
|
6722
7009
|
import { createInterface } from "readline";
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
|
|
6736
|
-
|
|
6737
|
-
|
|
6738
|
-
|
|
6739
|
-
|
|
6740
|
-
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
|
|
6830
|
-
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
7010
|
+
var log12, EXTERNAL_TOOL_TIMEOUT_MS, USER_FACING_TOOLS, HeadlessSession;
|
|
7011
|
+
var init_headless = __esm({
|
|
7012
|
+
"src/headless/index.ts"() {
|
|
7013
|
+
"use strict";
|
|
7014
|
+
init_logger();
|
|
7015
|
+
init_config();
|
|
7016
|
+
init_prompt();
|
|
7017
|
+
init_trigger();
|
|
7018
|
+
init_compaction();
|
|
7019
|
+
init_lsp();
|
|
7020
|
+
init_agent();
|
|
7021
|
+
init_session();
|
|
7022
|
+
init_toolRegistry();
|
|
7023
|
+
init_attachments();
|
|
7024
|
+
init_planFile();
|
|
7025
|
+
init_stats();
|
|
7026
|
+
init_messageQueue();
|
|
7027
|
+
init_resolve();
|
|
7028
|
+
init_sentinel();
|
|
7029
|
+
log12 = createLogger("headless");
|
|
7030
|
+
EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
|
|
7031
|
+
USER_FACING_TOOLS = /* @__PURE__ */ new Set([
|
|
7032
|
+
"promptUser",
|
|
7033
|
+
"confirmDestructiveAction",
|
|
7034
|
+
"presentPublishPlan"
|
|
7035
|
+
]);
|
|
7036
|
+
HeadlessSession = class {
|
|
7037
|
+
// Configuration
|
|
7038
|
+
opts;
|
|
7039
|
+
config;
|
|
7040
|
+
// Conversation state
|
|
7041
|
+
state = createAgentState();
|
|
7042
|
+
sessionStats = createSessionStats();
|
|
7043
|
+
// Turn lifecycle
|
|
7044
|
+
running = false;
|
|
7045
|
+
currentAbort = null;
|
|
7046
|
+
/** RequestId of the in-flight message command — injected into streamed events. */
|
|
7047
|
+
currentRequestId;
|
|
7048
|
+
/** Guard: track whether terminal `completed` was already sent so we emit exactly one. */
|
|
7049
|
+
completedEmitted = false;
|
|
7050
|
+
turnStart = 0;
|
|
7051
|
+
/**
|
|
7052
|
+
* Onboarding state of the currently-running turn. Captured at runSingleTurn
|
|
7053
|
+
* start so onBackgroundComplete can enqueue background results with the
|
|
7054
|
+
* right state (the triggering turn's state, not a stale one).
|
|
7055
|
+
*/
|
|
7056
|
+
currentOnboardingState;
|
|
7057
|
+
/**
|
|
7058
|
+
* Unified message queue. Holds pending work to deliver after the current
|
|
7059
|
+
* turn completes: chained automated actions, background sub-agent results,
|
|
7060
|
+
* and user messages sent while a turn is running. Strict FIFO. Persisted
|
|
7061
|
+
* to .remy-stats.json so queued work survives process restarts.
|
|
7062
|
+
*/
|
|
7063
|
+
queue;
|
|
7064
|
+
// External tool bridge
|
|
7065
|
+
pendingTools = /* @__PURE__ */ new Map();
|
|
7066
|
+
earlyResults = /* @__PURE__ */ new Map();
|
|
7067
|
+
// Tool block updates from background completions (separate from the message queue)
|
|
7068
|
+
pendingBlockUpdates = [];
|
|
7069
|
+
// Tool lifecycle management — shared across all nesting depths
|
|
7070
|
+
toolRegistry = new ToolRegistry();
|
|
7071
|
+
// IO
|
|
7072
|
+
readline = null;
|
|
7073
|
+
constructor(opts = {}) {
|
|
7074
|
+
this.opts = opts;
|
|
7075
|
+
}
|
|
7076
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7077
|
+
// Lifecycle
|
|
7078
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7079
|
+
async start() {
|
|
7080
|
+
const stderrWrite = (...args2) => {
|
|
7081
|
+
process.stderr.write(args2.map(String).join(" ") + "\n");
|
|
7082
|
+
};
|
|
7083
|
+
console.log = stderrWrite;
|
|
7084
|
+
console.warn = stderrWrite;
|
|
7085
|
+
console.info = stderrWrite;
|
|
7086
|
+
if (this.opts.lspUrl) {
|
|
7087
|
+
setLspBaseUrl(this.opts.lspUrl);
|
|
7088
|
+
}
|
|
7089
|
+
this.config = resolveConfig({
|
|
7090
|
+
apiKey: this.opts.apiKey,
|
|
7091
|
+
baseUrl: this.opts.baseUrl
|
|
7092
|
+
});
|
|
7093
|
+
const resumed = loadSession(this.state);
|
|
7094
|
+
this.queue = new MessageQueue(loadQueue(), () => this.persistStats());
|
|
7095
|
+
if (resumed) {
|
|
7096
|
+
this.emit("session_restored", {
|
|
7097
|
+
messageCount: this.state.messages.length,
|
|
7098
|
+
...this.queueFields()
|
|
7099
|
+
});
|
|
7100
|
+
}
|
|
7101
|
+
this.toolRegistry.onEvent = this.onEvent;
|
|
7102
|
+
this.readline = createInterface({ input: process.stdin });
|
|
7103
|
+
this.readline.on("line", this.handleStdinLine);
|
|
7104
|
+
this.readline.on("close", () => {
|
|
7105
|
+
this.emit("stopping");
|
|
7106
|
+
this.emit("stopped");
|
|
7107
|
+
process.exit(0);
|
|
7108
|
+
});
|
|
7109
|
+
process.on("SIGTERM", this.shutdown);
|
|
7110
|
+
process.on("SIGINT", this.shutdown);
|
|
7111
|
+
this.emit("ready", this.queueFields());
|
|
7112
|
+
}
|
|
7113
|
+
shutdown = () => {
|
|
7114
|
+
this.emit("stopping");
|
|
7115
|
+
this.emit("stopped");
|
|
7116
|
+
process.exit(0);
|
|
7117
|
+
};
|
|
7118
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7119
|
+
// Wire protocol
|
|
7120
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7121
|
+
emit(event, data, requestId) {
|
|
7122
|
+
const payload = { event, ...data };
|
|
7123
|
+
if (requestId) {
|
|
7124
|
+
payload.requestId = requestId;
|
|
7125
|
+
}
|
|
7126
|
+
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
7127
|
+
}
|
|
7128
|
+
/**
|
|
7129
|
+
* Emit a `completed` event and mark completedEmitted. Includes
|
|
7130
|
+
* `queuedMessages` if the queue has items (sandbox uses this to know the
|
|
7131
|
+
* agent is still busy with pipeline work).
|
|
7132
|
+
*/
|
|
7133
|
+
emitCompleted(rid, data) {
|
|
7134
|
+
this.emit("completed", { ...data, ...this.queueFields() }, rid);
|
|
7135
|
+
this.completedEmitted = true;
|
|
7136
|
+
}
|
|
7137
|
+
/** Returns `{ queuedMessages }` when the queue is non-empty, else empty object. */
|
|
7138
|
+
queueFields() {
|
|
7139
|
+
return this.queue.length > 0 ? { queuedMessages: this.queue.snapshot() } : {};
|
|
7140
|
+
}
|
|
7141
|
+
/** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
|
|
7142
|
+
dispatchSimple(requestId, eventName, handler) {
|
|
7143
|
+
try {
|
|
7144
|
+
const data = handler();
|
|
7145
|
+
if (eventName) {
|
|
7146
|
+
this.emit(eventName, data, requestId);
|
|
7147
|
+
}
|
|
7148
|
+
this.emit("completed", { success: true }, requestId);
|
|
7149
|
+
} catch (err) {
|
|
7150
|
+
this.emit("completed", { success: false, error: err.message }, requestId);
|
|
7151
|
+
}
|
|
7152
|
+
}
|
|
7153
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7154
|
+
// Stats + queue persistence
|
|
7155
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7156
|
+
/** Persist sessionStats + queue snapshot to .remy-stats.json. */
|
|
7157
|
+
persistStats() {
|
|
7158
|
+
this.sessionStats.updatedAt = Date.now();
|
|
7159
|
+
writeStats(this.sessionStats, this.queue.snapshot());
|
|
7160
|
+
}
|
|
7161
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7162
|
+
// Background completions (tool-block mutation; message delivery via queue)
|
|
7163
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7164
|
+
/** Apply queued tool block updates to state.messages. Safe to call any time. */
|
|
7165
|
+
applyPendingBlockUpdates() {
|
|
7166
|
+
if (this.pendingBlockUpdates.length === 0) {
|
|
7167
|
+
return;
|
|
6834
7168
|
}
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
7169
|
+
const updates = this.pendingBlockUpdates.splice(0);
|
|
7170
|
+
for (const update of updates) {
|
|
7171
|
+
for (const msg of this.state.messages) {
|
|
7172
|
+
if (!Array.isArray(msg.content)) {
|
|
7173
|
+
continue;
|
|
7174
|
+
}
|
|
7175
|
+
for (const block of msg.content) {
|
|
7176
|
+
if (block.type === "tool" && block.id === update.toolCallId) {
|
|
7177
|
+
block.backgroundResult = update.result;
|
|
7178
|
+
block.completedAt = Date.now();
|
|
7179
|
+
if (update.subAgentMessages) {
|
|
7180
|
+
block.subAgentMessages = update.subAgentMessages;
|
|
7181
|
+
}
|
|
7182
|
+
}
|
|
6841
7183
|
}
|
|
6842
7184
|
}
|
|
6843
7185
|
}
|
|
7186
|
+
saveSession(this.state);
|
|
6844
7187
|
}
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
return;
|
|
6851
|
-
}
|
|
6852
|
-
const idx = findSafeInsertionPoint(state.messages);
|
|
6853
|
-
state.messages.splice(idx, 0, ...summaries);
|
|
6854
|
-
saveSession(state);
|
|
6855
|
-
}
|
|
6856
|
-
function onBackgroundComplete(toolCallId, name, result, subAgentMessages) {
|
|
6857
|
-
pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
|
|
6858
|
-
log11.info("Background complete", {
|
|
6859
|
-
toolCallId,
|
|
6860
|
-
name,
|
|
6861
|
-
requestId: currentRequestId
|
|
6862
|
-
});
|
|
6863
|
-
onEvent({
|
|
6864
|
-
type: "tool_background_complete",
|
|
6865
|
-
id: toolCallId,
|
|
6866
|
-
name,
|
|
6867
|
-
result
|
|
6868
|
-
});
|
|
6869
|
-
backgroundQueue.push({
|
|
6870
|
-
toolCallId,
|
|
6871
|
-
name,
|
|
6872
|
-
result,
|
|
6873
|
-
completedAt: Date.now()
|
|
6874
|
-
});
|
|
6875
|
-
if (!running) {
|
|
6876
|
-
applyPendingBlockUpdates();
|
|
6877
|
-
flushBackgroundQueue();
|
|
6878
|
-
}
|
|
6879
|
-
}
|
|
6880
|
-
const USER_FACING_TOOLS = /* @__PURE__ */ new Set([
|
|
6881
|
-
"promptUser",
|
|
6882
|
-
"confirmDestructiveAction",
|
|
6883
|
-
"presentPublishPlan"
|
|
6884
|
-
]);
|
|
6885
|
-
function resolveExternalTool(id, name, _input) {
|
|
6886
|
-
const early = earlyResults.get(id);
|
|
6887
|
-
if (early !== void 0) {
|
|
6888
|
-
earlyResults.delete(id);
|
|
6889
|
-
return Promise.resolve(early);
|
|
6890
|
-
}
|
|
6891
|
-
const shouldTimeout = !USER_FACING_TOOLS.has(name);
|
|
6892
|
-
return new Promise((resolve2) => {
|
|
6893
|
-
const timeout = shouldTimeout ? setTimeout(() => {
|
|
6894
|
-
pendingTools.delete(id);
|
|
6895
|
-
resolve2(
|
|
6896
|
-
"Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
|
|
6897
|
-
);
|
|
6898
|
-
}, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
|
|
6899
|
-
pendingTools.set(id, {
|
|
6900
|
-
resolve: (result) => {
|
|
6901
|
-
clearTimeout(timeout);
|
|
6902
|
-
resolve2(result);
|
|
6903
|
-
},
|
|
6904
|
-
timeout
|
|
6905
|
-
});
|
|
6906
|
-
});
|
|
6907
|
-
}
|
|
6908
|
-
function onEvent(e) {
|
|
6909
|
-
const rid = currentRequestId;
|
|
6910
|
-
switch (e.type) {
|
|
6911
|
-
case "turn_started":
|
|
6912
|
-
emit("turn_started", {}, rid);
|
|
6913
|
-
return;
|
|
6914
|
-
// Terminal events — translate to `completed`
|
|
6915
|
-
case "turn_done":
|
|
6916
|
-
completedEmitted = true;
|
|
6917
|
-
if (e.stats) {
|
|
6918
|
-
sessionStats.turns++;
|
|
6919
|
-
sessionStats.totalInputTokens += e.stats.inputTokens;
|
|
6920
|
-
sessionStats.totalOutputTokens += e.stats.outputTokens;
|
|
6921
|
-
sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
|
|
6922
|
-
sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
|
|
6923
|
-
sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
|
|
6924
|
-
}
|
|
6925
|
-
sessionStats.messageCount = state.messages.length;
|
|
6926
|
-
sessionStats.updatedAt = Date.now();
|
|
6927
|
-
try {
|
|
6928
|
-
writeFileSync(".remy-stats.json", JSON.stringify(sessionStats));
|
|
6929
|
-
} catch {
|
|
7188
|
+
/** Drain pending compaction summaries and insert at a safe point. */
|
|
7189
|
+
applyPendingSummaries() {
|
|
7190
|
+
const summaries = getPendingSummaries();
|
|
7191
|
+
if (summaries.length === 0) {
|
|
7192
|
+
return;
|
|
6930
7193
|
}
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
7194
|
+
const idx = findSafeInsertionPoint(this.state.messages);
|
|
7195
|
+
this.state.messages.splice(idx, 0, ...summaries);
|
|
7196
|
+
saveSession(this.state);
|
|
7197
|
+
}
|
|
7198
|
+
onBackgroundComplete = (toolCallId, name, result, subAgentMessages) => {
|
|
7199
|
+
this.pendingBlockUpdates.push({ toolCallId, result, subAgentMessages });
|
|
7200
|
+
log12.info("Background complete", {
|
|
7201
|
+
toolCallId,
|
|
7202
|
+
name,
|
|
7203
|
+
requestId: this.currentRequestId
|
|
7204
|
+
});
|
|
7205
|
+
this.onEvent({
|
|
7206
|
+
type: "tool_background_complete",
|
|
7207
|
+
id: toolCallId,
|
|
7208
|
+
name,
|
|
7209
|
+
result
|
|
7210
|
+
});
|
|
7211
|
+
this.queue.push({
|
|
7212
|
+
command: {
|
|
7213
|
+
action: "message",
|
|
7214
|
+
text: buildBackgroundResultsMessage([{ toolCallId, name, result }]),
|
|
7215
|
+
...this.currentOnboardingState && {
|
|
7216
|
+
onboardingState: this.currentOnboardingState
|
|
7217
|
+
}
|
|
7218
|
+
},
|
|
7219
|
+
source: "background",
|
|
7220
|
+
enqueuedAt: Date.now()
|
|
7221
|
+
});
|
|
7222
|
+
if (!this.running) {
|
|
7223
|
+
this.applyPendingBlockUpdates();
|
|
7224
|
+
this.kickDrain();
|
|
7225
|
+
}
|
|
7226
|
+
};
|
|
7227
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7228
|
+
// External tool bridge
|
|
7229
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7230
|
+
resolveExternalTool = (id, name, _input) => {
|
|
7231
|
+
const early = this.earlyResults.get(id);
|
|
7232
|
+
if (early !== void 0) {
|
|
7233
|
+
this.earlyResults.delete(id);
|
|
7234
|
+
return Promise.resolve(early);
|
|
7235
|
+
}
|
|
7236
|
+
const shouldTimeout = !USER_FACING_TOOLS.has(name);
|
|
7237
|
+
return new Promise((resolve2) => {
|
|
7238
|
+
const timeout = shouldTimeout ? setTimeout(() => {
|
|
7239
|
+
this.pendingTools.delete(id);
|
|
7240
|
+
resolve2(
|
|
7241
|
+
"Error: Tool timed out \u2014 no response from the app environment after 5 minutes."
|
|
7242
|
+
);
|
|
7243
|
+
}, EXTERNAL_TOOL_TIMEOUT_MS) : void 0;
|
|
7244
|
+
this.pendingTools.set(id, {
|
|
7245
|
+
resolve: (result) => {
|
|
7246
|
+
clearTimeout(timeout);
|
|
7247
|
+
resolve2(result);
|
|
7248
|
+
},
|
|
7249
|
+
timeout
|
|
7250
|
+
});
|
|
7251
|
+
});
|
|
7252
|
+
};
|
|
7253
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7254
|
+
// AgentEvent → wire protocol translation
|
|
7255
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7256
|
+
onEvent = (e) => {
|
|
7257
|
+
const rid = this.currentRequestId;
|
|
7258
|
+
switch (e.type) {
|
|
7259
|
+
case "turn_started":
|
|
7260
|
+
this.emit("turn_started", {}, rid);
|
|
7261
|
+
return;
|
|
7262
|
+
case "user_message":
|
|
7263
|
+
this.emit("user_message", { text: e.text }, rid);
|
|
7264
|
+
return;
|
|
7265
|
+
// Terminal events — translate to `completed`.
|
|
7266
|
+
// Post-turn queue drain happens in handleMessage AFTER runTurn returns,
|
|
7267
|
+
// so that `running` is held across the drain and no user message can
|
|
7268
|
+
// slip in mid-pipeline.
|
|
7269
|
+
case "turn_done":
|
|
7270
|
+
if (e.stats) {
|
|
7271
|
+
this.sessionStats.turns++;
|
|
7272
|
+
this.sessionStats.totalInputTokens += e.stats.inputTokens;
|
|
7273
|
+
this.sessionStats.totalOutputTokens += e.stats.outputTokens;
|
|
7274
|
+
this.sessionStats.totalCacheCreationTokens += e.stats.cacheCreationTokens ?? 0;
|
|
7275
|
+
this.sessionStats.totalCacheReadTokens += e.stats.cacheReadTokens ?? 0;
|
|
7276
|
+
this.sessionStats.lastContextSize = e.stats.lastCallInputTokens ?? e.stats.inputTokens;
|
|
7277
|
+
}
|
|
7278
|
+
this.sessionStats.messageCount = this.state.messages.length;
|
|
7279
|
+
this.persistStats();
|
|
7280
|
+
this.emitCompleted(rid, {
|
|
7281
|
+
success: true,
|
|
7282
|
+
durationMs: Date.now() - this.turnStart
|
|
7283
|
+
});
|
|
7284
|
+
return;
|
|
7285
|
+
case "turn_cancelled": {
|
|
7286
|
+
this.emit(
|
|
7287
|
+
"completed",
|
|
7288
|
+
{ success: false, error: "cancelled", ...this.queueFields() },
|
|
7289
|
+
rid
|
|
6946
7290
|
);
|
|
7291
|
+
this.completedEmitted = true;
|
|
7292
|
+
return;
|
|
6947
7293
|
}
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
const base = name.slice(0, name.length - ext.length);
|
|
7083
|
-
let counter = 1;
|
|
7084
|
-
while (existsSync(join(UPLOADS_DIR, `${base}-${counter}${ext}`))) {
|
|
7085
|
-
counter++;
|
|
7086
|
-
}
|
|
7087
|
-
return `${base}-${counter}${ext}`;
|
|
7088
|
-
}
|
|
7089
|
-
const IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
7090
|
-
".png",
|
|
7091
|
-
".jpg",
|
|
7092
|
-
".jpeg",
|
|
7093
|
-
".gif",
|
|
7094
|
-
".webp",
|
|
7095
|
-
".svg",
|
|
7096
|
-
".bmp",
|
|
7097
|
-
".ico",
|
|
7098
|
-
".tiff",
|
|
7099
|
-
".tif",
|
|
7100
|
-
".avif",
|
|
7101
|
-
".heic",
|
|
7102
|
-
".heif"
|
|
7103
|
-
]);
|
|
7104
|
-
function isImageAttachment(att) {
|
|
7105
|
-
const name = att.filename || filenameFromUrl(att.url);
|
|
7106
|
-
return IMAGE_EXTENSIONS.has(extname(name).toLowerCase());
|
|
7107
|
-
}
|
|
7108
|
-
async function persistAttachments(attachments) {
|
|
7109
|
-
const nonVoice = attachments.filter((a) => !a.isVoice);
|
|
7110
|
-
if (nonVoice.length === 0) {
|
|
7111
|
-
return { documents: [], images: [] };
|
|
7112
|
-
}
|
|
7113
|
-
mkdirSync(UPLOADS_DIR, { recursive: true });
|
|
7114
|
-
const results = await Promise.allSettled(
|
|
7115
|
-
nonVoice.map(async (att) => {
|
|
7116
|
-
const name = resolveUniqueFilename(
|
|
7117
|
-
att.filename || filenameFromUrl(att.url)
|
|
7118
|
-
);
|
|
7119
|
-
const localPath = join(UPLOADS_DIR, name);
|
|
7120
|
-
const res = await fetch(att.url, {
|
|
7121
|
-
signal: AbortSignal.timeout(3e4)
|
|
7122
|
-
});
|
|
7123
|
-
if (!res.ok) {
|
|
7124
|
-
throw new Error(`HTTP ${res.status} downloading ${att.url}`);
|
|
7125
|
-
}
|
|
7126
|
-
const buffer = Buffer.from(await res.arrayBuffer());
|
|
7127
|
-
await writeFile(localPath, buffer);
|
|
7128
|
-
log11.info("Attachment saved", {
|
|
7129
|
-
filename: name,
|
|
7130
|
-
path: localPath,
|
|
7131
|
-
bytes: buffer.length
|
|
7132
|
-
});
|
|
7133
|
-
let extractedTextPath;
|
|
7134
|
-
if (att.extractedTextUrl) {
|
|
7294
|
+
// Streaming events — forward with requestId
|
|
7295
|
+
case "text":
|
|
7296
|
+
this.emit(
|
|
7297
|
+
"text",
|
|
7298
|
+
{
|
|
7299
|
+
text: e.text,
|
|
7300
|
+
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
7301
|
+
},
|
|
7302
|
+
rid
|
|
7303
|
+
);
|
|
7304
|
+
return;
|
|
7305
|
+
case "thinking":
|
|
7306
|
+
this.emit(
|
|
7307
|
+
"thinking",
|
|
7308
|
+
{
|
|
7309
|
+
text: e.text,
|
|
7310
|
+
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
7311
|
+
},
|
|
7312
|
+
rid
|
|
7313
|
+
);
|
|
7314
|
+
return;
|
|
7315
|
+
case "tool_input_delta":
|
|
7316
|
+
this.emit(
|
|
7317
|
+
"tool_input_delta",
|
|
7318
|
+
{
|
|
7319
|
+
id: e.id,
|
|
7320
|
+
name: e.name,
|
|
7321
|
+
result: e.result,
|
|
7322
|
+
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
7323
|
+
},
|
|
7324
|
+
rid
|
|
7325
|
+
);
|
|
7326
|
+
return;
|
|
7327
|
+
case "tool_start":
|
|
7328
|
+
this.emit(
|
|
7329
|
+
"tool_start",
|
|
7330
|
+
{
|
|
7331
|
+
id: e.id,
|
|
7332
|
+
name: e.name,
|
|
7333
|
+
input: e.input,
|
|
7334
|
+
...e.partial && { partial: true },
|
|
7335
|
+
...e.background && { background: true },
|
|
7336
|
+
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
7337
|
+
},
|
|
7338
|
+
rid
|
|
7339
|
+
);
|
|
7340
|
+
return;
|
|
7341
|
+
case "tool_done":
|
|
7342
|
+
this.emit(
|
|
7343
|
+
"tool_done",
|
|
7344
|
+
{
|
|
7345
|
+
id: e.id,
|
|
7346
|
+
name: e.name,
|
|
7347
|
+
result: e.result,
|
|
7348
|
+
isError: e.isError,
|
|
7349
|
+
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
7350
|
+
},
|
|
7351
|
+
rid
|
|
7352
|
+
);
|
|
7353
|
+
return;
|
|
7354
|
+
case "tool_background_complete":
|
|
7355
|
+
this.emit(
|
|
7356
|
+
"tool_background_complete",
|
|
7357
|
+
{
|
|
7358
|
+
id: e.id,
|
|
7359
|
+
name: e.name,
|
|
7360
|
+
result: e.result,
|
|
7361
|
+
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
7362
|
+
},
|
|
7363
|
+
rid
|
|
7364
|
+
);
|
|
7365
|
+
return;
|
|
7366
|
+
case "tool_stopped":
|
|
7367
|
+
this.emit(
|
|
7368
|
+
"tool_stopped",
|
|
7369
|
+
{
|
|
7370
|
+
id: e.id,
|
|
7371
|
+
name: e.name,
|
|
7372
|
+
mode: e.mode,
|
|
7373
|
+
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
7374
|
+
},
|
|
7375
|
+
rid
|
|
7376
|
+
);
|
|
7377
|
+
return;
|
|
7378
|
+
case "tool_restarted":
|
|
7379
|
+
this.emit(
|
|
7380
|
+
"tool_restarted",
|
|
7381
|
+
{
|
|
7382
|
+
id: e.id,
|
|
7383
|
+
name: e.name,
|
|
7384
|
+
input: e.input,
|
|
7385
|
+
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
7386
|
+
},
|
|
7387
|
+
rid
|
|
7388
|
+
);
|
|
7389
|
+
return;
|
|
7390
|
+
case "status":
|
|
7391
|
+
this.emit(
|
|
7392
|
+
"status",
|
|
7393
|
+
{
|
|
7394
|
+
message: e.message,
|
|
7395
|
+
...e.parentToolId && { parentToolId: e.parentToolId }
|
|
7396
|
+
},
|
|
7397
|
+
rid
|
|
7398
|
+
);
|
|
7399
|
+
return;
|
|
7400
|
+
case "error":
|
|
7401
|
+
this.emit("error", { error: e.error }, rid);
|
|
7402
|
+
return;
|
|
7403
|
+
}
|
|
7404
|
+
};
|
|
7405
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7406
|
+
// Message command handler (long-running / streaming)
|
|
7407
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7408
|
+
/**
|
|
7409
|
+
* Run one turn (without acquiring the `running` lock). Called by
|
|
7410
|
+
* handleMessage for the initial turn, then repeatedly for each queued
|
|
7411
|
+
* message — `running` stays held across the queue drain so no user
|
|
7412
|
+
* message can slip in mid-pipeline.
|
|
7413
|
+
*/
|
|
7414
|
+
async runSingleTurn(parsed, requestId) {
|
|
7415
|
+
this.currentRequestId = requestId;
|
|
7416
|
+
this.currentAbort = new AbortController();
|
|
7417
|
+
this.completedEmitted = false;
|
|
7418
|
+
this.turnStart = Date.now();
|
|
7419
|
+
const attachments = parsed.attachments;
|
|
7420
|
+
if (attachments?.length) {
|
|
7421
|
+
log12.info("Message has attachments", {
|
|
7422
|
+
count: attachments.length,
|
|
7423
|
+
urls: attachments.map((a) => a.url)
|
|
7424
|
+
});
|
|
7425
|
+
}
|
|
7426
|
+
let userMessage = parsed.text ?? "";
|
|
7427
|
+
if (attachments?.some((a) => !a.isVoice)) {
|
|
7135
7428
|
try {
|
|
7136
|
-
const
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
if (
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7429
|
+
const { documents, images } = await persistAttachments(attachments);
|
|
7430
|
+
const all = [...documents, ...images];
|
|
7431
|
+
const header = buildUploadHeader(all);
|
|
7432
|
+
if (header) {
|
|
7433
|
+
userMessage = userMessage ? `${header}
|
|
7434
|
+
|
|
7435
|
+
${userMessage}` : header;
|
|
7143
7436
|
}
|
|
7144
|
-
} catch {
|
|
7437
|
+
} catch (err) {
|
|
7438
|
+
log12.warn("Attachment persistence failed", { error: err.message });
|
|
7145
7439
|
}
|
|
7146
7440
|
}
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
images: settled.filter((s) => s.isImage).map((s) => s.result)
|
|
7157
|
-
};
|
|
7158
|
-
}
|
|
7159
|
-
function buildUploadHeader(results) {
|
|
7160
|
-
const succeeded = results.filter(Boolean);
|
|
7161
|
-
if (succeeded.length === 0) {
|
|
7162
|
-
return "";
|
|
7163
|
-
}
|
|
7164
|
-
if (succeeded.length === 1) {
|
|
7165
|
-
const r = succeeded[0];
|
|
7166
|
-
const parts = [`[Uploaded file: ${r.localPath}`];
|
|
7167
|
-
if (r.extractedTextPath) {
|
|
7168
|
-
parts.push(`extracted text: ${r.extractedTextPath}`);
|
|
7169
|
-
}
|
|
7170
|
-
return parts.join(" \u2014 ") + "]";
|
|
7171
|
-
}
|
|
7172
|
-
const lines = succeeded.map((r) => {
|
|
7173
|
-
if (r.extractedTextPath) {
|
|
7174
|
-
return `- ${r.localPath} (extracted text: ${r.extractedTextPath})`;
|
|
7175
|
-
}
|
|
7176
|
-
return `- ${r.localPath}`;
|
|
7177
|
-
});
|
|
7178
|
-
return `[Uploaded files]
|
|
7179
|
-
${lines.join("\n")}`;
|
|
7180
|
-
}
|
|
7181
|
-
async function handleMessage(parsed, requestId) {
|
|
7182
|
-
if (running) {
|
|
7183
|
-
emit(
|
|
7184
|
-
"error",
|
|
7185
|
-
{ error: "Agent is already processing a message" },
|
|
7186
|
-
requestId
|
|
7187
|
-
);
|
|
7188
|
-
emit(
|
|
7189
|
-
"completed",
|
|
7190
|
-
{ success: false, error: "Agent is already processing a message" },
|
|
7191
|
-
requestId
|
|
7192
|
-
);
|
|
7193
|
-
return;
|
|
7194
|
-
}
|
|
7195
|
-
running = true;
|
|
7196
|
-
currentRequestId = requestId;
|
|
7197
|
-
currentAbort = new AbortController();
|
|
7198
|
-
completedEmitted = false;
|
|
7199
|
-
turnStart = Date.now();
|
|
7200
|
-
const attachments = parsed.attachments;
|
|
7201
|
-
if (attachments?.length) {
|
|
7202
|
-
log11.info("Message has attachments", {
|
|
7203
|
-
count: attachments.length,
|
|
7204
|
-
urls: attachments.map((a) => a.url)
|
|
7205
|
-
});
|
|
7206
|
-
}
|
|
7207
|
-
let userMessage = parsed.text ?? "";
|
|
7208
|
-
if (attachments?.some((a) => !a.isVoice)) {
|
|
7209
|
-
try {
|
|
7210
|
-
const { documents, images } = await persistAttachments(attachments);
|
|
7211
|
-
const all = [...documents, ...images];
|
|
7212
|
-
const header = buildUploadHeader(all);
|
|
7213
|
-
if (header) {
|
|
7214
|
-
userMessage = userMessage ? `${header}
|
|
7215
|
-
|
|
7216
|
-
${userMessage}` : header;
|
|
7441
|
+
let resolved = null;
|
|
7442
|
+
try {
|
|
7443
|
+
resolved = resolveAction(userMessage);
|
|
7444
|
+
} catch (err) {
|
|
7445
|
+
this.emitCompleted(requestId, {
|
|
7446
|
+
success: false,
|
|
7447
|
+
error: err.message || "Failed to resolve action"
|
|
7448
|
+
});
|
|
7449
|
+
return;
|
|
7217
7450
|
}
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
{ success: false, error: err.message || "Failed to resolve action" },
|
|
7229
|
-
requestId
|
|
7230
|
-
);
|
|
7231
|
-
return;
|
|
7232
|
-
}
|
|
7233
|
-
pendingNextAction = void 0;
|
|
7234
|
-
if (resolved !== null) {
|
|
7235
|
-
userMessage = resolved.message;
|
|
7236
|
-
pendingNextAction = resolved.next;
|
|
7237
|
-
}
|
|
7238
|
-
const isHidden = resolved !== null || !!parsed.hidden;
|
|
7239
|
-
const rawText = parsed.text ?? "";
|
|
7240
|
-
if (rawText.startsWith("@@automated::approvePlan@@")) {
|
|
7241
|
-
try {
|
|
7242
|
-
const plan = readFileSync(".remy-plan.md", "utf-8");
|
|
7243
|
-
writeFileSync(
|
|
7244
|
-
".remy-plan.md",
|
|
7245
|
-
plan.replace(/^status:\s*pending/m, "status: approved"),
|
|
7246
|
-
"utf-8"
|
|
7451
|
+
if (resolved !== null) {
|
|
7452
|
+
userMessage = resolved.message;
|
|
7453
|
+
}
|
|
7454
|
+
const isHidden = resolved !== null || !!parsed.hidden;
|
|
7455
|
+
applyPlanFileSideEffect(parsed.text ?? "");
|
|
7456
|
+
const onboardingState = parsed.onboardingState ?? "onboardingFinished";
|
|
7457
|
+
this.currentOnboardingState = onboardingState;
|
|
7458
|
+
const system = buildSystemPrompt(
|
|
7459
|
+
onboardingState,
|
|
7460
|
+
parsed.viewContext
|
|
7247
7461
|
);
|
|
7248
|
-
|
|
7462
|
+
if (resolved?.next) {
|
|
7463
|
+
this.queue.push({
|
|
7464
|
+
command: {
|
|
7465
|
+
action: "message",
|
|
7466
|
+
text: sentinel(resolved.next),
|
|
7467
|
+
onboardingState
|
|
7468
|
+
},
|
|
7469
|
+
source: "chain",
|
|
7470
|
+
enqueuedAt: Date.now()
|
|
7471
|
+
});
|
|
7472
|
+
}
|
|
7473
|
+
try {
|
|
7474
|
+
await runTurn({
|
|
7475
|
+
state: this.state,
|
|
7476
|
+
userMessage,
|
|
7477
|
+
attachments,
|
|
7478
|
+
apiConfig: this.config,
|
|
7479
|
+
system,
|
|
7480
|
+
model: this.opts.model,
|
|
7481
|
+
onboardingState,
|
|
7482
|
+
requestId,
|
|
7483
|
+
signal: this.currentAbort.signal,
|
|
7484
|
+
onEvent: this.onEvent,
|
|
7485
|
+
resolveExternalTool: this.resolveExternalTool,
|
|
7486
|
+
hidden: isHidden,
|
|
7487
|
+
toolRegistry: this.toolRegistry,
|
|
7488
|
+
onBackgroundComplete: this.onBackgroundComplete
|
|
7489
|
+
});
|
|
7490
|
+
if (!this.completedEmitted) {
|
|
7491
|
+
this.emitCompleted(requestId, {
|
|
7492
|
+
success: false,
|
|
7493
|
+
error: "Turn ended unexpectedly"
|
|
7494
|
+
});
|
|
7495
|
+
}
|
|
7496
|
+
log12.info("Turn complete", {
|
|
7497
|
+
requestId,
|
|
7498
|
+
durationMs: Date.now() - this.turnStart
|
|
7499
|
+
});
|
|
7500
|
+
} catch (err) {
|
|
7501
|
+
if (!this.completedEmitted) {
|
|
7502
|
+
this.emit("error", { error: err.message }, requestId);
|
|
7503
|
+
this.emitCompleted(requestId, {
|
|
7504
|
+
success: false,
|
|
7505
|
+
error: err.message
|
|
7506
|
+
});
|
|
7507
|
+
}
|
|
7508
|
+
log12.warn("Command failed", {
|
|
7509
|
+
action: "message",
|
|
7510
|
+
requestId,
|
|
7511
|
+
error: err.message
|
|
7512
|
+
});
|
|
7513
|
+
this.queue.drain();
|
|
7514
|
+
}
|
|
7515
|
+
this.applyPendingSummaries();
|
|
7516
|
+
this.applyPendingBlockUpdates();
|
|
7249
7517
|
}
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7518
|
+
async handleMessage(parsed, requestId) {
|
|
7519
|
+
if (this.running) {
|
|
7520
|
+
const command = { ...parsed };
|
|
7521
|
+
if (requestId && command.requestId === void 0) {
|
|
7522
|
+
command.requestId = requestId;
|
|
7523
|
+
}
|
|
7524
|
+
this.queue.push({
|
|
7525
|
+
command,
|
|
7526
|
+
source: "user",
|
|
7527
|
+
enqueuedAt: Date.now()
|
|
7528
|
+
});
|
|
7529
|
+
this.emit(
|
|
7530
|
+
"queued",
|
|
7531
|
+
{ position: this.queue.length, ...this.queueFields() },
|
|
7532
|
+
requestId
|
|
7533
|
+
);
|
|
7534
|
+
return;
|
|
7535
|
+
}
|
|
7536
|
+
this.running = true;
|
|
7537
|
+
try {
|
|
7538
|
+
await this.runSingleTurn(parsed, requestId);
|
|
7539
|
+
await this.drainQueueLoop();
|
|
7540
|
+
} finally {
|
|
7541
|
+
this.currentAbort = null;
|
|
7542
|
+
this.currentRequestId = void 0;
|
|
7543
|
+
this.running = false;
|
|
7544
|
+
}
|
|
7254
7545
|
}
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7264
|
-
|
|
7265
|
-
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
model: opts.model,
|
|
7269
|
-
onboardingState,
|
|
7270
|
-
requestId,
|
|
7271
|
-
signal: currentAbort.signal,
|
|
7272
|
-
onEvent,
|
|
7273
|
-
resolveExternalTool,
|
|
7274
|
-
hidden: isHidden,
|
|
7275
|
-
toolRegistry,
|
|
7276
|
-
onBackgroundComplete
|
|
7277
|
-
});
|
|
7278
|
-
if (!completedEmitted) {
|
|
7279
|
-
emit(
|
|
7280
|
-
"completed",
|
|
7281
|
-
{ success: false, error: "Turn ended unexpectedly" },
|
|
7282
|
-
requestId
|
|
7283
|
-
);
|
|
7546
|
+
/**
|
|
7547
|
+
* Drain the queue in strict FIFO order. Caller must hold `running = true`.
|
|
7548
|
+
* User messages arriving during the drain will be enqueued behind current items.
|
|
7549
|
+
*/
|
|
7550
|
+
async drainQueueLoop() {
|
|
7551
|
+
while (true) {
|
|
7552
|
+
const next = this.queue.shift();
|
|
7553
|
+
if (!next) {
|
|
7554
|
+
break;
|
|
7555
|
+
}
|
|
7556
|
+
const nextRid = next.command.requestId ?? `${next.source}-${Date.now()}`;
|
|
7557
|
+
await this.runSingleTurn(next.command, nextRid);
|
|
7558
|
+
}
|
|
7284
7559
|
}
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7560
|
+
/**
|
|
7561
|
+
* Resume draining the queue when the agent is idle. Acquires the lock,
|
|
7562
|
+
* drains, releases. Used by the `resume` stdin action (sandbox-initiated)
|
|
7563
|
+
* and by kickDrain (background-completion-initiated).
|
|
7564
|
+
*/
|
|
7565
|
+
async resumeQueue() {
|
|
7566
|
+
if (this.running || this.queue.length === 0) {
|
|
7567
|
+
return;
|
|
7568
|
+
}
|
|
7569
|
+
this.running = true;
|
|
7570
|
+
try {
|
|
7571
|
+
await this.drainQueueLoop();
|
|
7572
|
+
} finally {
|
|
7573
|
+
this.currentAbort = null;
|
|
7574
|
+
this.currentRequestId = void 0;
|
|
7575
|
+
this.running = false;
|
|
7576
|
+
}
|
|
7293
7577
|
}
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
const rl = createInterface({ input: process.stdin });
|
|
7305
|
-
rl.on("line", async (line) => {
|
|
7306
|
-
let parsed;
|
|
7307
|
-
try {
|
|
7308
|
-
parsed = JSON.parse(line);
|
|
7309
|
-
} catch {
|
|
7310
|
-
emit("error", { error: "Invalid JSON on stdin" });
|
|
7311
|
-
return;
|
|
7312
|
-
}
|
|
7313
|
-
const { action, requestId } = parsed;
|
|
7314
|
-
log11.info("Command received", { action, requestId });
|
|
7315
|
-
if (action === "tool_result" && parsed.id) {
|
|
7316
|
-
const id = parsed.id;
|
|
7317
|
-
const result = parsed.result ?? "";
|
|
7318
|
-
const pending = pendingTools.get(id);
|
|
7319
|
-
if (pending) {
|
|
7320
|
-
pendingTools.delete(id);
|
|
7321
|
-
pending.resolve(result);
|
|
7322
|
-
} else if (!running) {
|
|
7323
|
-
log11.info("Late tool_result while idle, dismissing", { id });
|
|
7324
|
-
emit("completed", { success: true }, requestId);
|
|
7325
|
-
} else {
|
|
7326
|
-
earlyResults.set(id, result);
|
|
7578
|
+
/**
|
|
7579
|
+
* Kick off drainage of the queue when the agent is idle. Used by
|
|
7580
|
+
* onBackgroundComplete (when !running) to deliver results without
|
|
7581
|
+
* racing any currently-synchronous path.
|
|
7582
|
+
*/
|
|
7583
|
+
kickDrain() {
|
|
7584
|
+
if (this.running || this.queue.length === 0) {
|
|
7585
|
+
return;
|
|
7586
|
+
}
|
|
7587
|
+
setTimeout(() => this.resumeQueue(), 0);
|
|
7327
7588
|
}
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
running,
|
|
7335
|
-
...running && currentRequestId ? { currentRequestId } : {}
|
|
7336
|
-
}));
|
|
7337
|
-
return;
|
|
7338
|
-
}
|
|
7339
|
-
if (action === "clear") {
|
|
7340
|
-
dispatchSimple(requestId, "session_cleared", () => handleClear(state));
|
|
7341
|
-
return;
|
|
7342
|
-
}
|
|
7343
|
-
if (action === "cancel") {
|
|
7344
|
-
handleCancel(currentAbort, pendingTools);
|
|
7345
|
-
emit("completed", { success: true }, requestId);
|
|
7346
|
-
return;
|
|
7347
|
-
}
|
|
7348
|
-
if (action === "stop_tool") {
|
|
7349
|
-
const id = parsed.id;
|
|
7350
|
-
const mode = parsed.mode ?? "hard";
|
|
7351
|
-
const found = toolRegistry.stop(id, mode);
|
|
7352
|
-
if (found) {
|
|
7353
|
-
emit("completed", { success: true }, requestId);
|
|
7354
|
-
} else {
|
|
7355
|
-
emit(
|
|
7356
|
-
"completed",
|
|
7357
|
-
{ success: false, error: "Tool not found" },
|
|
7358
|
-
requestId
|
|
7359
|
-
);
|
|
7589
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7590
|
+
// Simple command handlers
|
|
7591
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7592
|
+
handleClear() {
|
|
7593
|
+
clearSession(this.state);
|
|
7594
|
+
return {};
|
|
7360
7595
|
}
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
{ success: false, error: "Tool not found" },
|
|
7373
|
-
requestId
|
|
7374
|
-
);
|
|
7596
|
+
/** Cancel the running turn and drain the queue. Returns the drained items. */
|
|
7597
|
+
handleCancel() {
|
|
7598
|
+
if (this.currentAbort) {
|
|
7599
|
+
this.currentAbort.abort();
|
|
7600
|
+
}
|
|
7601
|
+
for (const [id, pending] of this.pendingTools) {
|
|
7602
|
+
clearTimeout(pending.timeout);
|
|
7603
|
+
pending.resolve("Error: cancelled");
|
|
7604
|
+
this.pendingTools.delete(id);
|
|
7605
|
+
}
|
|
7606
|
+
return this.queue.drain();
|
|
7375
7607
|
}
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7608
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7609
|
+
// Stdin router
|
|
7610
|
+
//////////////////////////////////////////////////////////////////////////////
|
|
7611
|
+
handleStdinLine = async (line) => {
|
|
7612
|
+
let parsed;
|
|
7613
|
+
try {
|
|
7614
|
+
parsed = JSON.parse(line);
|
|
7615
|
+
} catch {
|
|
7616
|
+
this.emit("error", { error: "Invalid JSON on stdin" });
|
|
7617
|
+
return;
|
|
7618
|
+
}
|
|
7619
|
+
const { action, requestId } = parsed;
|
|
7620
|
+
log12.info("Command received", { action, requestId });
|
|
7621
|
+
if (action === "tool_result" && parsed.id) {
|
|
7622
|
+
const id = parsed.id;
|
|
7623
|
+
const result = parsed.result ?? "";
|
|
7624
|
+
const pending = this.pendingTools.get(id);
|
|
7625
|
+
if (pending) {
|
|
7626
|
+
this.pendingTools.delete(id);
|
|
7627
|
+
pending.resolve(result);
|
|
7628
|
+
} else if (!this.running) {
|
|
7629
|
+
log12.info("Late tool_result while idle, dismissing", { id });
|
|
7630
|
+
this.emit("completed", { success: true }, requestId);
|
|
7631
|
+
} else {
|
|
7632
|
+
this.earlyResults.set(id, result);
|
|
7386
7633
|
}
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7634
|
+
return;
|
|
7635
|
+
}
|
|
7636
|
+
if (action === "get_history") {
|
|
7637
|
+
this.applyPendingBlockUpdates();
|
|
7638
|
+
this.dispatchSimple(requestId, "history", () => ({
|
|
7639
|
+
messages: this.state.messages,
|
|
7640
|
+
running: this.running,
|
|
7641
|
+
...this.running && this.currentRequestId ? { currentRequestId: this.currentRequestId } : {},
|
|
7642
|
+
...this.queueFields()
|
|
7643
|
+
}));
|
|
7644
|
+
return;
|
|
7645
|
+
}
|
|
7646
|
+
if (action === "clear") {
|
|
7647
|
+
this.dispatchSimple(
|
|
7648
|
+
requestId,
|
|
7649
|
+
"session_cleared",
|
|
7650
|
+
() => this.handleClear()
|
|
7651
|
+
);
|
|
7652
|
+
return;
|
|
7653
|
+
}
|
|
7654
|
+
if (action === "cancel") {
|
|
7655
|
+
const cancelled = this.handleCancel();
|
|
7656
|
+
this.emit(
|
|
7657
|
+
"completed",
|
|
7658
|
+
{
|
|
7659
|
+
success: true,
|
|
7660
|
+
...cancelled.length > 0 && { cancelledMessages: cancelled }
|
|
7661
|
+
},
|
|
7662
|
+
requestId
|
|
7663
|
+
);
|
|
7664
|
+
return;
|
|
7665
|
+
}
|
|
7666
|
+
if (action === "stop_tool") {
|
|
7667
|
+
const id = parsed.id;
|
|
7668
|
+
const mode = parsed.mode ?? "hard";
|
|
7669
|
+
const found = this.toolRegistry.stop(id, mode);
|
|
7670
|
+
if (found) {
|
|
7671
|
+
this.emit("completed", { success: true }, requestId);
|
|
7672
|
+
} else {
|
|
7673
|
+
this.emit(
|
|
7674
|
+
"completed",
|
|
7675
|
+
{ success: false, error: "Tool not found" },
|
|
7676
|
+
requestId
|
|
7677
|
+
);
|
|
7391
7678
|
}
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7679
|
+
return;
|
|
7680
|
+
}
|
|
7681
|
+
if (action === "restart_tool") {
|
|
7682
|
+
const id = parsed.id;
|
|
7683
|
+
const patchedInput = parsed.input;
|
|
7684
|
+
const found = this.toolRegistry.restart(id, patchedInput);
|
|
7685
|
+
if (found) {
|
|
7686
|
+
this.emit("completed", { success: true }, requestId);
|
|
7687
|
+
} else {
|
|
7688
|
+
this.emit(
|
|
7689
|
+
"completed",
|
|
7690
|
+
{ success: false, error: "Tool not found" },
|
|
7691
|
+
requestId
|
|
7692
|
+
);
|
|
7406
7693
|
}
|
|
7694
|
+
return;
|
|
7407
7695
|
}
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7696
|
+
if (action === "compact") {
|
|
7697
|
+
triggerCompaction(this.state, this.config, {
|
|
7698
|
+
onStart: () => {
|
|
7699
|
+
this.sessionStats.compactionInProgress = true;
|
|
7700
|
+
this.persistStats();
|
|
7701
|
+
},
|
|
7702
|
+
onSummariesReady: () => {
|
|
7703
|
+
if (!this.running) {
|
|
7704
|
+
this.applyPendingSummaries();
|
|
7705
|
+
}
|
|
7706
|
+
this.emit("compaction_complete", {}, requestId);
|
|
7707
|
+
this.emit("completed", { success: true }, requestId);
|
|
7708
|
+
},
|
|
7709
|
+
onError: (error) => {
|
|
7710
|
+
this.emit("compaction_complete", { error }, requestId);
|
|
7711
|
+
this.emit("completed", { success: false, error }, requestId);
|
|
7712
|
+
},
|
|
7713
|
+
onFinally: () => {
|
|
7714
|
+
this.sessionStats.compactionInProgress = false;
|
|
7715
|
+
this.sessionStats.lastContextSize = 0;
|
|
7716
|
+
this.sessionStats.messageCount = this.state.messages.length;
|
|
7717
|
+
this.persistStats();
|
|
7718
|
+
}
|
|
7719
|
+
});
|
|
7720
|
+
return;
|
|
7721
|
+
}
|
|
7722
|
+
if (action === "message") {
|
|
7723
|
+
await this.handleMessage(parsed, requestId);
|
|
7724
|
+
return;
|
|
7725
|
+
}
|
|
7726
|
+
if (action === "resume") {
|
|
7727
|
+
if (this.running) {
|
|
7728
|
+
this.emit(
|
|
7729
|
+
"completed",
|
|
7730
|
+
{ success: false, error: "already running" },
|
|
7731
|
+
requestId
|
|
7732
|
+
);
|
|
7733
|
+
return;
|
|
7734
|
+
}
|
|
7735
|
+
if (this.queue.length === 0) {
|
|
7736
|
+
this.emit("completed", { success: true }, requestId);
|
|
7737
|
+
return;
|
|
7738
|
+
}
|
|
7739
|
+
this.emit("completed", { success: true }, requestId);
|
|
7740
|
+
await this.resumeQueue();
|
|
7741
|
+
return;
|
|
7742
|
+
}
|
|
7743
|
+
this.emit("error", { error: `Unknown action: ${action}` }, requestId);
|
|
7744
|
+
this.emit(
|
|
7745
|
+
"completed",
|
|
7746
|
+
{ success: false, error: `Unknown action: ${action}` },
|
|
7747
|
+
requestId
|
|
7748
|
+
);
|
|
7749
|
+
};
|
|
7750
|
+
};
|
|
7451
7751
|
}
|
|
7452
7752
|
});
|
|
7453
7753
|
|
|
@@ -7795,13 +8095,13 @@ function printDebugInfo(config) {
|
|
|
7795
8095
|
var logLevel = flags.logLevel || void 0;
|
|
7796
8096
|
if (headless) {
|
|
7797
8097
|
initLoggerHeadless(logLevel);
|
|
7798
|
-
const {
|
|
7799
|
-
|
|
8098
|
+
const { HeadlessSession: HeadlessSession2 } = await Promise.resolve().then(() => (init_headless(), headless_exports));
|
|
8099
|
+
new HeadlessSession2({
|
|
7800
8100
|
apiKey: flags.apiKey,
|
|
7801
8101
|
baseUrl: flags.baseUrl,
|
|
7802
8102
|
model: flags.model,
|
|
7803
8103
|
lspUrl: flags.lspUrl
|
|
7804
|
-
}).catch((err) => {
|
|
8104
|
+
}).start().catch((err) => {
|
|
7805
8105
|
console.error(err.message);
|
|
7806
8106
|
process.exit(1);
|
|
7807
8107
|
});
|