@mindstudio-ai/remy 0.1.260 → 0.1.262
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/README.md +3 -1
- package/dist/headless.d.ts +70 -9
- package/dist/headless.js +282 -113
- package/dist/index.js +283 -114
- package/dist/prompt/static/instructions.md +1 -1
- package/dist/prompt/static/team.md +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -264,6 +264,7 @@ The headless IPC protocol uses request correlation and a unified response patter
|
|
|
264
264
|
- Every stdout response to a command includes the same `requestId`
|
|
265
265
|
- System events (lifecycle, shutdown) never have a `requestId`
|
|
266
266
|
- Every command ends with exactly one `completed` event: `{event:"completed", requestId, success, error?}`
|
|
267
|
+
- Messages sent while a turn is running are queued. When the turn ends, all contiguous queued user messages and background results are delivered together as **one merged turn**: the first queued message's `requestId` becomes the turn's primary id (stamped on `turn_started` and all streaming events), each absorbed message echoes its own `user_message` with its original `requestId` and `queued: true`, and at turn end the primary `completed` is emitted first, followed immediately by one `completed {…same outcome, absorbed: true}` per other absorbed `requestId`. Automated-action (`@@automated::…@@`) messages and chain steps never merge — they always run one turn each.
|
|
267
268
|
- The caller distinguishes command responses from system events with a single check: `if (msg.requestId)`
|
|
268
269
|
|
|
269
270
|
This enables a simple promise-based RPC layer: send a command with a unique ID, store a pending promise keyed by that ID, resolve it when you see `completed` with the matching ID.
|
|
@@ -364,6 +365,7 @@ All command responses include the `requestId` from the originating command.
|
|
|
364
365
|
|-------|--------|-------------|
|
|
365
366
|
| `text` | `text`, `parentToolId?` | Streaming text chunk |
|
|
366
367
|
| `thinking` | `text`, `parentToolId?` | Agent's internal reasoning |
|
|
368
|
+
| `user_message` | `text`, `attachments?`, `queued?` | Echo of a user message entering the turn. Queue-delivered messages carry `queued: true` and their own original `requestId` (a merged turn emits one per absorbed message); idle sends echo with the turn's requestId and no `queued` flag. |
|
|
367
369
|
| `tool_start` | `id`, `name`, `input`, `partial?`, `parentToolId?` | Tool execution started. `partial: true` means more `tool_start` events will follow for this id (progressive input streaming). |
|
|
368
370
|
| `tool_input_delta` | `id`, `name`, `result`, `parentToolId?` | Progressive tool content (streaming tools only) |
|
|
369
371
|
| `tool_done` | `id`, `name`, `result`, `isError`, `parentToolId?` | Tool execution completed |
|
|
@@ -372,7 +374,7 @@ All command responses include the `requestId` from the originating command.
|
|
|
372
374
|
| `history` | `messages` | Response to `get_history` |
|
|
373
375
|
| `session_cleared` | | Response to `clear` |
|
|
374
376
|
| `models_changed` | `models?`, `modelSurfaces`, `allowedModelsByType` | Response to `changeModels` |
|
|
375
|
-
| `completed` | `success`, `error?` | Terminal event — exactly one per command |
|
|
377
|
+
| `completed` | `success`, `error?`, `absorbed?` | Terminal event — exactly one per command. For a merged turn, the primary requestId's `completed` comes first, then one `{absorbed: true}` completed per other absorbed requestId with the same outcome — consumers should resolve their pending request but skip turn-lifecycle handling for absorbed terminals. |
|
|
376
378
|
|
|
377
379
|
#### Example Session
|
|
378
380
|
|
package/dist/headless.d.ts
CHANGED
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* - System events (ready, session_restored, stopping, stopped) never have a requestId.
|
|
12
12
|
* - Every command ends with exactly one `completed` event:
|
|
13
13
|
* {event:"completed", requestId, success:true|false, error?:string}
|
|
14
|
+
* - When contiguous queued user/background messages merge into one turn, the
|
|
15
|
+
* turn's primary requestId gets the real `completed` first, then each other
|
|
16
|
+
* absorbed requestId gets one `{...same outcome, absorbed:true}` completed
|
|
17
|
+
* immediately after — one terminal per command either way.
|
|
14
18
|
* - `tool_result` is fire-and-forget (resolves an in-flight promise, no completed event).
|
|
15
19
|
*
|
|
16
20
|
* `get_history` is paginated. Request: {action:"get_history", before?:number,
|
|
@@ -46,8 +50,12 @@ declare class HeadlessSession {
|
|
|
46
50
|
private currentAbort;
|
|
47
51
|
/** RequestId of the in-flight message command — injected into streamed events. */
|
|
48
52
|
private currentRequestId;
|
|
49
|
-
/** Guard: track whether terminal `completed` was already sent so we emit
|
|
53
|
+
/** Guard: track whether terminal `completed` was already sent so we emit
|
|
54
|
+
* exactly one per requestId. */
|
|
50
55
|
private completedEmitted;
|
|
56
|
+
/** Outcome of the current turn's primary `completed` — read after the turn
|
|
57
|
+
* to stamp the same success/error onto absorbed requestIds' terminals. */
|
|
58
|
+
private lastCompleted;
|
|
51
59
|
private turnStart;
|
|
52
60
|
/**
|
|
53
61
|
* Onboarding state of the currently-running turn. Captured at runSingleTurn
|
|
@@ -74,6 +82,9 @@ declare class HeadlessSession {
|
|
|
74
82
|
/** Emit a `completed` event and mark completedEmitted. Queue state is
|
|
75
83
|
* surfaced separately via the `queue_changed` event, not on `completed`. */
|
|
76
84
|
private emitCompleted;
|
|
85
|
+
/** Outcome of the turn's primary `completed`, for stamping onto absorbed
|
|
86
|
+
* requestIds' terminals. Falls back to failure if none was emitted. */
|
|
87
|
+
private primaryOutcome;
|
|
77
88
|
/** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
|
|
78
89
|
private dispatchSimple;
|
|
79
90
|
/** Persist sessionStats + queue snapshot to .remy-stats.json. */
|
|
@@ -98,20 +109,65 @@ declare class HeadlessSession {
|
|
|
98
109
|
private resolveExternalTool;
|
|
99
110
|
private onEvent;
|
|
100
111
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
112
|
+
* Persist one entry's non-voice uploads to disk and build its header. The
|
|
113
|
+
* header tells the LLM where to read each file; it's kept separate so it
|
|
114
|
+
* gets injected at API-send time and never persisted into the user's chat
|
|
115
|
+
* content (which would leak into history restore on the frontend).
|
|
116
|
+
*
|
|
117
|
+
* Must be awaited sequentially across entries — persistAttachments'
|
|
118
|
+
* filename de-dup set is per-call, so parallel calls race on names.
|
|
119
|
+
*/
|
|
120
|
+
private persistEntryAttachments;
|
|
121
|
+
/**
|
|
122
|
+
* Run one turn for a single command (without acquiring the `running` lock).
|
|
123
|
+
* Owns the per-command machinery: @@automated:: action resolution, plan-file
|
|
124
|
+
* and buildModel side effects, and chain expansion — which is why
|
|
125
|
+
* sentinel-bearing commands always come through here, one turn each, never
|
|
126
|
+
* merged. The turn itself runs in executeTurn.
|
|
105
127
|
*/
|
|
106
128
|
private runSingleTurn;
|
|
129
|
+
/**
|
|
130
|
+
* Run a mailbox batch — contiguous queued user + background items — as one
|
|
131
|
+
* merged turn. Every item becomes its own history entry and user_message
|
|
132
|
+
* event (own requestId, attachments, hidden flag); adjacent background
|
|
133
|
+
* items fold into a single background_results entry so the LLM sees one
|
|
134
|
+
* combined block. No action-sentinel machinery here: batch construction
|
|
135
|
+
* guarantees none (sentinel-bearing user items are drain barriers that run
|
|
136
|
+
* alone via runSingleTurn, and background_results is a NON_ACTION sentinel
|
|
137
|
+
* with no side effects).
|
|
138
|
+
*/
|
|
139
|
+
private runMergedTurn;
|
|
140
|
+
/**
|
|
141
|
+
* Run one agent turn over the given entries (without acquiring the
|
|
142
|
+
* `running` lock). Owns the turn-generic lifecycle: request bookkeeping,
|
|
143
|
+
* the forced-compaction gate, runTurn error handling, and terminal
|
|
144
|
+
* `completed` events — the primary requestId's completed first, then one
|
|
145
|
+
* `{absorbed: true}` completed per other absorbed requestId with the same
|
|
146
|
+
* outcome, on every exit path (done, cancel, error, unexpected), so every
|
|
147
|
+
* queued message's caller resolves.
|
|
148
|
+
*/
|
|
149
|
+
private executeTurn;
|
|
107
150
|
private handleMessage;
|
|
151
|
+
/**
|
|
152
|
+
* True for queued user items whose text is an @@automated:: action message.
|
|
153
|
+
* These key per-item raw-text side effects in runSingleTurn (resolveAction,
|
|
154
|
+
* plan file, buildModel, chain expansion) — they always run alone, one turn
|
|
155
|
+
* each. Background items are sentinel-formatted too but background_results
|
|
156
|
+
* is NON_ACTION and side-effect-free, so they merge freely.
|
|
157
|
+
*/
|
|
158
|
+
private isDrainBarrier;
|
|
108
159
|
/**
|
|
109
160
|
* Drain the queue in strict FIFO order. Caller must hold `running = true`.
|
|
110
161
|
* User messages arriving during the drain will be enqueued behind current items.
|
|
111
162
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
163
|
+
* The queue serves two purposes with opposite delivery semantics:
|
|
164
|
+
* - Sequencer: chain steps and sentinel-bearing user items are pipeline
|
|
165
|
+
* stages — one item, one turn, nothing merged in.
|
|
166
|
+
* - Mailbox: plain user messages and background results are accumulated
|
|
167
|
+
* context and intent — everything contiguous flushes together into ONE
|
|
168
|
+
* merged turn, so the model reconciles all of it at once instead of
|
|
169
|
+
* burning a full turn per item (and possibly executing instructions a
|
|
170
|
+
* later queued message already amended).
|
|
115
171
|
*/
|
|
116
172
|
private drainQueueLoop;
|
|
117
173
|
/**
|
|
@@ -136,9 +192,14 @@ declare class HeadlessSession {
|
|
|
136
192
|
* Cancel the running turn and flush the follow-ups that belonged to it
|
|
137
193
|
* (`chain`/`background`), while preserving `source: 'user'` items — those are
|
|
138
194
|
* independent user intent, not tied to the aborted run. The preserved user
|
|
139
|
-
* messages run next: `
|
|
195
|
+
* messages run next: `executeTurn` swallows the abort, so `handleMessage`
|
|
140
196
|
* falls through to `drainQueueLoop` with `running` still held. Returns the
|
|
141
197
|
* flushed items (for the cancel command's resume/discard UX).
|
|
198
|
+
*
|
|
199
|
+
* Messages already absorbed into the in-flight merged turn are NOT
|
|
200
|
+
* preserved — they were delivered into the turn that's being cancelled and
|
|
201
|
+
* each gets a `{cancelled, absorbed:true}` terminal. Only items still
|
|
202
|
+
* sitting in the queue survive.
|
|
142
203
|
*/
|
|
143
204
|
private handleCancel;
|
|
144
205
|
/**
|
package/dist/headless.js
CHANGED
|
@@ -483,13 +483,17 @@ var ALLOWED_MODELS_BY_TYPE = {
|
|
|
483
483
|
"gemini-3.1-pro",
|
|
484
484
|
"gemini-3-flash",
|
|
485
485
|
"gemini-3.5-flash",
|
|
486
|
+
"gemini-3.7-flash",
|
|
486
487
|
"grok-build-0.1",
|
|
487
488
|
"grok-4.5",
|
|
489
|
+
"grok-4.6",
|
|
488
490
|
"glm-5.2",
|
|
489
491
|
"muse-spark-1.1",
|
|
490
492
|
"kimi-k2-7-code",
|
|
491
493
|
"kimi-k3",
|
|
492
|
-
"deepseek-v4-flash-0731"
|
|
494
|
+
"deepseek-v4-flash-0731",
|
|
495
|
+
"qwen3.8-2.4t-a95b-deepinfra",
|
|
496
|
+
"minimax-m3"
|
|
493
497
|
]
|
|
494
498
|
// vision: undefined — unconstrained
|
|
495
499
|
// image_generation: undefined — unconstrained
|
|
@@ -7614,9 +7618,7 @@ function createAgentState() {
|
|
|
7614
7618
|
async function runTurn(params) {
|
|
7615
7619
|
const {
|
|
7616
7620
|
state,
|
|
7617
|
-
|
|
7618
|
-
attachments,
|
|
7619
|
-
attachmentHeader,
|
|
7621
|
+
entries,
|
|
7620
7622
|
apiConfig,
|
|
7621
7623
|
system,
|
|
7622
7624
|
model,
|
|
@@ -7625,7 +7627,6 @@ async function runTurn(params) {
|
|
|
7625
7627
|
signal,
|
|
7626
7628
|
onEvent,
|
|
7627
7629
|
resolveExternalTool,
|
|
7628
|
-
hidden,
|
|
7629
7630
|
requestId,
|
|
7630
7631
|
toolRegistry,
|
|
7631
7632
|
onBackgroundComplete
|
|
@@ -7635,45 +7636,54 @@ async function runTurn(params) {
|
|
|
7635
7636
|
const baseline = resolveModel("parent", state.models, model);
|
|
7636
7637
|
const parentModel = buildModelOverride ?? baseline;
|
|
7637
7638
|
const modelOverride = buildModelOverride && buildModelOverride !== baseline ? { from: baseline } : void 0;
|
|
7639
|
+
const totalAttachments = entries.reduce(
|
|
7640
|
+
(n, e) => n + (e.attachments?.length ?? 0),
|
|
7641
|
+
0
|
|
7642
|
+
);
|
|
7638
7643
|
log14.info("Turn started", {
|
|
7639
7644
|
requestId,
|
|
7640
7645
|
model,
|
|
7641
7646
|
buildModel: buildModelOverride,
|
|
7642
7647
|
toolCount: tools2.length,
|
|
7643
|
-
...
|
|
7644
|
-
|
|
7645
|
-
}
|
|
7648
|
+
...entries.length > 1 && { entryCount: entries.length },
|
|
7649
|
+
...totalAttachments > 0 && { attachmentCount: totalAttachments }
|
|
7646
7650
|
});
|
|
7647
7651
|
onEvent({
|
|
7648
7652
|
type: "turn_started",
|
|
7649
7653
|
model: parentModel,
|
|
7650
7654
|
...modelOverride && { modelOverride }
|
|
7651
7655
|
});
|
|
7652
|
-
const
|
|
7653
|
-
|
|
7654
|
-
|
|
7656
|
+
const keptEntries = entries.filter(
|
|
7657
|
+
(e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
|
|
7658
|
+
);
|
|
7659
|
+
if (keptEntries.length === 0) {
|
|
7655
7660
|
onEvent({ type: "error", error: "Empty message" });
|
|
7656
7661
|
return;
|
|
7657
7662
|
}
|
|
7658
|
-
const
|
|
7659
|
-
|
|
7660
|
-
userMsg
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7663
|
+
for (const entry of keptEntries) {
|
|
7664
|
+
const hasAttachments = (entry.attachments?.length ?? 0) > 0;
|
|
7665
|
+
const userMsg = { role: "user", content: entry.text };
|
|
7666
|
+
if (entry.hidden) {
|
|
7667
|
+
userMsg.hidden = true;
|
|
7668
|
+
}
|
|
7669
|
+
if (hasAttachments) {
|
|
7670
|
+
userMsg.attachments = entry.attachments;
|
|
7671
|
+
}
|
|
7672
|
+
if (entry.attachmentHeader) {
|
|
7673
|
+
userMsg.attachmentHeader = entry.attachmentHeader;
|
|
7674
|
+
}
|
|
7675
|
+
state.messages.push(userMsg);
|
|
7676
|
+
onEvent({
|
|
7677
|
+
type: "user_message",
|
|
7678
|
+
text: entry.text,
|
|
7679
|
+
hidden: entry.hidden || void 0,
|
|
7680
|
+
// Include attachments so the live event can render a queued voice/image/file
|
|
7681
|
+
// bubble; a voice message has empty text and the transcript lives here.
|
|
7682
|
+
...hasAttachments && { attachments: entry.attachments },
|
|
7683
|
+
...entry.requestId && { requestId: entry.requestId },
|
|
7684
|
+
...entry.queued && { queued: true }
|
|
7685
|
+
});
|
|
7667
7686
|
}
|
|
7668
|
-
state.messages.push(userMsg);
|
|
7669
|
-
onEvent({
|
|
7670
|
-
type: "user_message",
|
|
7671
|
-
text: userMessage,
|
|
7672
|
-
hidden: hidden || void 0,
|
|
7673
|
-
// Include attachments so the live event can render a queued voice/image/file
|
|
7674
|
-
// bubble; a voice message has empty text and the transcript lives here.
|
|
7675
|
-
...hasAttachments && { attachments }
|
|
7676
|
-
});
|
|
7677
7687
|
const isFirstMessage = state.messages.filter((m) => m.role === "user").length === 1;
|
|
7678
7688
|
const STATUS_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
|
|
7679
7689
|
"setProjectOnboardingState",
|
|
@@ -7742,11 +7752,13 @@ async function runTurn(params) {
|
|
|
7742
7752
|
if (onboardingState && onboardingState !== "onboardingFinished") {
|
|
7743
7753
|
parts.push(`Build phase: ${onboardingState}`);
|
|
7744
7754
|
}
|
|
7745
|
-
const
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7755
|
+
for (const entry of keptEntries) {
|
|
7756
|
+
const automated = parseSentinel(entry.text);
|
|
7757
|
+
if (automated) {
|
|
7758
|
+
parts.push(`Automated action: ${automated.name}`);
|
|
7759
|
+
} else if (entry.text) {
|
|
7760
|
+
parts.push(`User request: ${entry.text.slice(-500)}`);
|
|
7761
|
+
}
|
|
7750
7762
|
}
|
|
7751
7763
|
return parts.join("\n");
|
|
7752
7764
|
},
|
|
@@ -8291,25 +8303,24 @@ async function persistAttachments(attachments) {
|
|
|
8291
8303
|
images: settled.filter((s) => s.isImage).map((s) => s.result)
|
|
8292
8304
|
};
|
|
8293
8305
|
}
|
|
8294
|
-
function buildUploadHeader(
|
|
8295
|
-
const
|
|
8296
|
-
|
|
8306
|
+
function buildUploadHeader(documents, images) {
|
|
8307
|
+
const entries = [
|
|
8308
|
+
...documents.filter((r) => r !== null).map((r) => ({ ...r, isImage: false })),
|
|
8309
|
+
...images.filter((r) => r !== null).map((r) => ({ ...r, isImage: true }))
|
|
8310
|
+
];
|
|
8311
|
+
if (entries.length === 0) {
|
|
8297
8312
|
return "";
|
|
8298
8313
|
}
|
|
8299
|
-
|
|
8300
|
-
|
|
8301
|
-
const
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
}
|
|
8305
|
-
return parts.join(" \u2014 ") + "]";
|
|
8314
|
+
const detail = (e) => e.extractedTextPath ? `extracted text: ${e.extractedTextPath}` : e.isImage ? null : "no extracted text \u2014 raw file only";
|
|
8315
|
+
if (entries.length === 1) {
|
|
8316
|
+
const e = entries[0];
|
|
8317
|
+
const extra = detail(e);
|
|
8318
|
+
return `[Uploaded file: ${e.localPath}${extra ? ` \u2014 ${extra}` : ""}]`;
|
|
8306
8319
|
}
|
|
8307
|
-
const lines =
|
|
8308
|
-
const
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
}
|
|
8312
|
-
return parts.join("\n");
|
|
8320
|
+
const lines = entries.map((e) => {
|
|
8321
|
+
const extra = detail(e);
|
|
8322
|
+
return `- ${e.localPath}${extra ? `
|
|
8323
|
+
${extra}` : ""}`;
|
|
8313
8324
|
});
|
|
8314
8325
|
return `[Uploaded files]
|
|
8315
8326
|
${lines.join("\n")}`;
|
|
@@ -8395,6 +8406,17 @@ var MessageQueue = class {
|
|
|
8395
8406
|
}
|
|
8396
8407
|
return item;
|
|
8397
8408
|
}
|
|
8409
|
+
/** Remove and return the first `n` items; fires onChange once. */
|
|
8410
|
+
shiftMany(n) {
|
|
8411
|
+
if (n <= 0) {
|
|
8412
|
+
return [];
|
|
8413
|
+
}
|
|
8414
|
+
const items = this.items.splice(0, n);
|
|
8415
|
+
if (items.length > 0) {
|
|
8416
|
+
this.onChange?.();
|
|
8417
|
+
}
|
|
8418
|
+
return items;
|
|
8419
|
+
}
|
|
8398
8420
|
/** Remove and return all queued items. */
|
|
8399
8421
|
drain() {
|
|
8400
8422
|
if (this.items.length === 0) {
|
|
@@ -8430,6 +8452,10 @@ var MessageQueue = class {
|
|
|
8430
8452
|
peek() {
|
|
8431
8453
|
return this.items[0];
|
|
8432
8454
|
}
|
|
8455
|
+
/** Return the item at index `i` without removing it. */
|
|
8456
|
+
peekAt(i) {
|
|
8457
|
+
return this.items[i];
|
|
8458
|
+
}
|
|
8433
8459
|
get length() {
|
|
8434
8460
|
return this.items.length;
|
|
8435
8461
|
}
|
|
@@ -8512,8 +8538,12 @@ var HeadlessSession = class {
|
|
|
8512
8538
|
currentAbort = null;
|
|
8513
8539
|
/** RequestId of the in-flight message command — injected into streamed events. */
|
|
8514
8540
|
currentRequestId;
|
|
8515
|
-
/** Guard: track whether terminal `completed` was already sent so we emit
|
|
8541
|
+
/** Guard: track whether terminal `completed` was already sent so we emit
|
|
8542
|
+
* exactly one per requestId. */
|
|
8516
8543
|
completedEmitted = false;
|
|
8544
|
+
/** Outcome of the current turn's primary `completed` — read after the turn
|
|
8545
|
+
* to stamp the same success/error onto absorbed requestIds' terminals. */
|
|
8546
|
+
lastCompleted = null;
|
|
8517
8547
|
turnStart = 0;
|
|
8518
8548
|
/**
|
|
8519
8549
|
* Onboarding state of the currently-running turn. Captured at runSingleTurn
|
|
@@ -8649,6 +8679,15 @@ var HeadlessSession = class {
|
|
|
8649
8679
|
emitCompleted(rid, data) {
|
|
8650
8680
|
this.emit("completed", { ...data }, rid);
|
|
8651
8681
|
this.completedEmitted = true;
|
|
8682
|
+
this.lastCompleted = {
|
|
8683
|
+
success: data.success === true,
|
|
8684
|
+
...typeof data.error === "string" && { error: data.error }
|
|
8685
|
+
};
|
|
8686
|
+
}
|
|
8687
|
+
/** Outcome of the turn's primary `completed`, for stamping onto absorbed
|
|
8688
|
+
* requestIds' terminals. Falls back to failure if none was emitted. */
|
|
8689
|
+
primaryOutcome() {
|
|
8690
|
+
return this.lastCompleted ?? { success: false };
|
|
8652
8691
|
}
|
|
8653
8692
|
/** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
|
|
8654
8693
|
dispatchSimple(requestId, eventName, handler) {
|
|
@@ -8807,9 +8846,14 @@ var HeadlessSession = class {
|
|
|
8807
8846
|
text: e.text,
|
|
8808
8847
|
// Forward attachments so queued voice/image/file sends render live;
|
|
8809
8848
|
// otherwise the bubble is blank until a get_history refresh.
|
|
8810
|
-
...e.attachments && { attachments: e.attachments }
|
|
8849
|
+
...e.attachments && { attachments: e.attachments },
|
|
8850
|
+
// Queue-delivered entries are flagged so the frontend renders the
|
|
8851
|
+
// echo (idle sends are rendered optimistically instead).
|
|
8852
|
+
...e.queued && { queued: true }
|
|
8811
8853
|
},
|
|
8812
|
-
|
|
8854
|
+
// A merged turn emits one user_message per absorbed entry — each
|
|
8855
|
+
// carries its own original requestId, not the turn's.
|
|
8856
|
+
e.requestId ?? rid
|
|
8813
8857
|
);
|
|
8814
8858
|
return;
|
|
8815
8859
|
// Terminal events — translate to `completed`.
|
|
@@ -8832,11 +8876,9 @@ var HeadlessSession = class {
|
|
|
8832
8876
|
durationMs: Date.now() - this.turnStart
|
|
8833
8877
|
});
|
|
8834
8878
|
return;
|
|
8835
|
-
case "turn_cancelled":
|
|
8836
|
-
this.
|
|
8837
|
-
this.completedEmitted = true;
|
|
8879
|
+
case "turn_cancelled":
|
|
8880
|
+
this.emitCompleted(rid, { success: false, error: "cancelled" });
|
|
8838
8881
|
return;
|
|
8839
|
-
}
|
|
8840
8882
|
// Streaming events — forward with requestId
|
|
8841
8883
|
case "text":
|
|
8842
8884
|
this.emit(
|
|
@@ -8956,17 +8998,34 @@ var HeadlessSession = class {
|
|
|
8956
8998
|
// Message command handler (long-running / streaming)
|
|
8957
8999
|
//////////////////////////////////////////////////////////////////////////////
|
|
8958
9000
|
/**
|
|
8959
|
-
*
|
|
8960
|
-
*
|
|
8961
|
-
*
|
|
8962
|
-
*
|
|
9001
|
+
* Persist one entry's non-voice uploads to disk and build its header. The
|
|
9002
|
+
* header tells the LLM where to read each file; it's kept separate so it
|
|
9003
|
+
* gets injected at API-send time and never persisted into the user's chat
|
|
9004
|
+
* content (which would leak into history restore on the frontend).
|
|
9005
|
+
*
|
|
9006
|
+
* Must be awaited sequentially across entries — persistAttachments'
|
|
9007
|
+
* filename de-dup set is per-call, so parallel calls race on names.
|
|
8963
9008
|
*/
|
|
8964
|
-
async
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
9009
|
+
async persistEntryAttachments(attachments) {
|
|
9010
|
+
if (!attachments?.some((a) => !a.isVoice)) {
|
|
9011
|
+
return void 0;
|
|
9012
|
+
}
|
|
9013
|
+
try {
|
|
9014
|
+
const { documents, images } = await persistAttachments(attachments);
|
|
9015
|
+
return buildUploadHeader(documents, images) || void 0;
|
|
9016
|
+
} catch (err) {
|
|
9017
|
+
log16.warn("Attachment persistence failed", { error: err.message });
|
|
9018
|
+
return void 0;
|
|
9019
|
+
}
|
|
9020
|
+
}
|
|
9021
|
+
/**
|
|
9022
|
+
* Run one turn for a single command (without acquiring the `running` lock).
|
|
9023
|
+
* Owns the per-command machinery: @@automated:: action resolution, plan-file
|
|
9024
|
+
* and buildModel side effects, and chain expansion — which is why
|
|
9025
|
+
* sentinel-bearing commands always come through here, one turn each, never
|
|
9026
|
+
* merged. The turn itself runs in executeTurn.
|
|
9027
|
+
*/
|
|
9028
|
+
async runSingleTurn(parsed, requestId, fromChain = false, queued = false) {
|
|
8970
9029
|
const attachments = parsed.attachments;
|
|
8971
9030
|
if (attachments?.length) {
|
|
8972
9031
|
log16.info("Message has attachments", {
|
|
@@ -8975,19 +9034,7 @@ var HeadlessSession = class {
|
|
|
8975
9034
|
});
|
|
8976
9035
|
}
|
|
8977
9036
|
let userMessage = parsed.text ?? "";
|
|
8978
|
-
|
|
8979
|
-
if (attachments?.some((a) => !a.isVoice)) {
|
|
8980
|
-
try {
|
|
8981
|
-
const { documents, images } = await persistAttachments(attachments);
|
|
8982
|
-
const all = [...documents, ...images];
|
|
8983
|
-
const header = buildUploadHeader(all);
|
|
8984
|
-
if (header) {
|
|
8985
|
-
attachmentHeader = header;
|
|
8986
|
-
}
|
|
8987
|
-
} catch (err) {
|
|
8988
|
-
log16.warn("Attachment persistence failed", { error: err.message });
|
|
8989
|
-
}
|
|
8990
|
-
}
|
|
9037
|
+
const attachmentHeader = await this.persistEntryAttachments(attachments);
|
|
8991
9038
|
let resolved = null;
|
|
8992
9039
|
try {
|
|
8993
9040
|
resolved = resolveAction(userMessage);
|
|
@@ -9024,22 +9071,108 @@ var HeadlessSession = class {
|
|
|
9024
9071
|
});
|
|
9025
9072
|
}
|
|
9026
9073
|
}
|
|
9074
|
+
await this.executeTurn({
|
|
9075
|
+
entries: [
|
|
9076
|
+
{
|
|
9077
|
+
text: userMessage,
|
|
9078
|
+
attachments,
|
|
9079
|
+
attachmentHeader,
|
|
9080
|
+
hidden: isHidden || void 0,
|
|
9081
|
+
requestId,
|
|
9082
|
+
queued: queued || void 0
|
|
9083
|
+
}
|
|
9084
|
+
],
|
|
9085
|
+
requestId,
|
|
9086
|
+
absorbedRids: [],
|
|
9087
|
+
onboardingState,
|
|
9088
|
+
system,
|
|
9089
|
+
buildModel
|
|
9090
|
+
});
|
|
9091
|
+
}
|
|
9092
|
+
/**
|
|
9093
|
+
* Run a mailbox batch — contiguous queued user + background items — as one
|
|
9094
|
+
* merged turn. Every item becomes its own history entry and user_message
|
|
9095
|
+
* event (own requestId, attachments, hidden flag); adjacent background
|
|
9096
|
+
* items fold into a single background_results entry so the LLM sees one
|
|
9097
|
+
* combined block. No action-sentinel machinery here: batch construction
|
|
9098
|
+
* guarantees none (sentinel-bearing user items are drain barriers that run
|
|
9099
|
+
* alone via runSingleTurn, and background_results is a NON_ACTION sentinel
|
|
9100
|
+
* with no side effects).
|
|
9101
|
+
*/
|
|
9102
|
+
async runMergedTurn(batch) {
|
|
9103
|
+
const primaryRid = batch[0].command.requestId ?? (batch.every((b) => b.source === "background") ? `background-${Date.now()}` : `merged-${Date.now()}`);
|
|
9104
|
+
const absorbedRids = batch.slice(1).map((b) => b.command.requestId).filter((rid) => typeof rid === "string");
|
|
9105
|
+
const entryList = [];
|
|
9106
|
+
for (const item of batch) {
|
|
9107
|
+
const text = item.command.text ?? "";
|
|
9108
|
+
const prev = entryList[entryList.length - 1];
|
|
9109
|
+
if (item.source === "background" && prev?.background) {
|
|
9110
|
+
prev.entry.text = mergeBackgroundResultsMessages([
|
|
9111
|
+
prev.entry.text,
|
|
9112
|
+
text
|
|
9113
|
+
]);
|
|
9114
|
+
continue;
|
|
9115
|
+
}
|
|
9116
|
+
entryList.push({
|
|
9117
|
+
background: item.source === "background",
|
|
9118
|
+
entry: {
|
|
9119
|
+
text,
|
|
9120
|
+
attachments: item.command.attachments,
|
|
9121
|
+
hidden: !!item.command.hidden || void 0,
|
|
9122
|
+
// Background items carry no requestId — their user_message falls
|
|
9123
|
+
// back to the turn's primary rid on the wire.
|
|
9124
|
+
requestId: item.command.requestId,
|
|
9125
|
+
queued: true
|
|
9126
|
+
}
|
|
9127
|
+
});
|
|
9128
|
+
}
|
|
9129
|
+
const entries = entryList.map((e) => e.entry);
|
|
9130
|
+
for (const entry of entries) {
|
|
9131
|
+
entry.attachmentHeader = await this.persistEntryAttachments(
|
|
9132
|
+
entry.attachments
|
|
9133
|
+
);
|
|
9134
|
+
}
|
|
9135
|
+
const onboardingState = batch.find((b) => b.command.onboardingState !== void 0)?.command.onboardingState ?? this.currentOnboardingState ?? "onboardingFinished";
|
|
9136
|
+
this.currentOnboardingState = onboardingState;
|
|
9137
|
+
const viewContext = [...batch].reverse().find((b) => b.command.viewContext !== void 0)?.command.viewContext;
|
|
9138
|
+
await this.executeTurn({
|
|
9139
|
+
entries,
|
|
9140
|
+
requestId: primaryRid,
|
|
9141
|
+
absorbedRids,
|
|
9142
|
+
onboardingState,
|
|
9143
|
+
system: buildSystemPrompt(onboardingState, viewContext)
|
|
9144
|
+
});
|
|
9145
|
+
}
|
|
9146
|
+
/**
|
|
9147
|
+
* Run one agent turn over the given entries (without acquiring the
|
|
9148
|
+
* `running` lock). Owns the turn-generic lifecycle: request bookkeeping,
|
|
9149
|
+
* the forced-compaction gate, runTurn error handling, and terminal
|
|
9150
|
+
* `completed` events — the primary requestId's completed first, then one
|
|
9151
|
+
* `{absorbed: true}` completed per other absorbed requestId with the same
|
|
9152
|
+
* outcome, on every exit path (done, cancel, error, unexpected), so every
|
|
9153
|
+
* queued message's caller resolves.
|
|
9154
|
+
*/
|
|
9155
|
+
async executeTurn(params) {
|
|
9156
|
+
const { entries, requestId, absorbedRids, onboardingState, system } = params;
|
|
9157
|
+
this.currentRequestId = requestId;
|
|
9158
|
+
this.currentAbort = new AbortController();
|
|
9159
|
+
this.completedEmitted = false;
|
|
9160
|
+
this.lastCompleted = null;
|
|
9161
|
+
this.turnStart = Date.now();
|
|
9162
|
+
await this.runForcedCompactionIfNeeded(requestId);
|
|
9027
9163
|
try {
|
|
9028
9164
|
await runTurn({
|
|
9029
9165
|
state: this.state,
|
|
9030
|
-
|
|
9031
|
-
attachments,
|
|
9032
|
-
attachmentHeader,
|
|
9166
|
+
entries,
|
|
9033
9167
|
apiConfig: this.config,
|
|
9034
9168
|
system,
|
|
9035
9169
|
model: this.opts.model,
|
|
9036
|
-
buildModel,
|
|
9170
|
+
buildModel: params.buildModel,
|
|
9037
9171
|
onboardingState,
|
|
9038
9172
|
requestId,
|
|
9039
9173
|
signal: this.currentAbort.signal,
|
|
9040
9174
|
onEvent: this.onEvent,
|
|
9041
9175
|
resolveExternalTool: this.resolveExternalTool,
|
|
9042
|
-
hidden: isHidden,
|
|
9043
9176
|
toolRegistry: this.toolRegistry,
|
|
9044
9177
|
onBackgroundComplete: this.onBackgroundComplete
|
|
9045
9178
|
});
|
|
@@ -9067,6 +9200,18 @@ var HeadlessSession = class {
|
|
|
9067
9200
|
error: err.message
|
|
9068
9201
|
});
|
|
9069
9202
|
}
|
|
9203
|
+
const outcome = this.primaryOutcome();
|
|
9204
|
+
for (const rid of absorbedRids) {
|
|
9205
|
+
this.emit(
|
|
9206
|
+
"completed",
|
|
9207
|
+
{
|
|
9208
|
+
success: outcome.success,
|
|
9209
|
+
...outcome.error && { error: outcome.error },
|
|
9210
|
+
absorbed: true
|
|
9211
|
+
},
|
|
9212
|
+
rid
|
|
9213
|
+
);
|
|
9214
|
+
}
|
|
9070
9215
|
applyPendingSummaries(this.state);
|
|
9071
9216
|
this.applyPendingBlockUpdates();
|
|
9072
9217
|
}
|
|
@@ -9093,42 +9238,61 @@ var HeadlessSession = class {
|
|
|
9093
9238
|
this.running = false;
|
|
9094
9239
|
}
|
|
9095
9240
|
}
|
|
9241
|
+
/**
|
|
9242
|
+
* True for queued user items whose text is an @@automated:: action message.
|
|
9243
|
+
* These key per-item raw-text side effects in runSingleTurn (resolveAction,
|
|
9244
|
+
* plan file, buildModel, chain expansion) — they always run alone, one turn
|
|
9245
|
+
* each. Background items are sentinel-formatted too but background_results
|
|
9246
|
+
* is NON_ACTION and side-effect-free, so they merge freely.
|
|
9247
|
+
*/
|
|
9248
|
+
isDrainBarrier(item) {
|
|
9249
|
+
return item.source === "user" && isAutomatedMessage(item.command.text ?? "");
|
|
9250
|
+
}
|
|
9096
9251
|
/**
|
|
9097
9252
|
* Drain the queue in strict FIFO order. Caller must hold `running = true`.
|
|
9098
9253
|
* User messages arriving during the drain will be enqueued behind current items.
|
|
9099
9254
|
*
|
|
9100
|
-
*
|
|
9101
|
-
*
|
|
9102
|
-
*
|
|
9255
|
+
* The queue serves two purposes with opposite delivery semantics:
|
|
9256
|
+
* - Sequencer: chain steps and sentinel-bearing user items are pipeline
|
|
9257
|
+
* stages — one item, one turn, nothing merged in.
|
|
9258
|
+
* - Mailbox: plain user messages and background results are accumulated
|
|
9259
|
+
* context and intent — everything contiguous flushes together into ONE
|
|
9260
|
+
* merged turn, so the model reconciles all of it at once instead of
|
|
9261
|
+
* burning a full turn per item (and possibly executing instructions a
|
|
9262
|
+
* later queued message already amended).
|
|
9103
9263
|
*/
|
|
9104
9264
|
async drainQueueLoop() {
|
|
9105
|
-
while (
|
|
9106
|
-
const
|
|
9107
|
-
if (
|
|
9108
|
-
|
|
9265
|
+
while (this.queue.length > 0) {
|
|
9266
|
+
const head = this.queue.peek();
|
|
9267
|
+
if (head.source === "chain") {
|
|
9268
|
+
const item = this.queue.shift();
|
|
9269
|
+
const rid = item.command.requestId ?? `chain-${Date.now()}`;
|
|
9270
|
+
await this.runSingleTurn(item.command, rid, true);
|
|
9271
|
+
continue;
|
|
9109
9272
|
}
|
|
9110
|
-
if (
|
|
9111
|
-
const
|
|
9112
|
-
|
|
9113
|
-
|
|
9114
|
-
if (more) {
|
|
9115
|
-
batch.push(more);
|
|
9116
|
-
}
|
|
9117
|
-
}
|
|
9118
|
-
const combinedCommand = {
|
|
9119
|
-
action: "message",
|
|
9120
|
-
text: mergeBackgroundResultsMessages(
|
|
9121
|
-
batch.map((b) => b.command.text ?? "")
|
|
9122
|
-
),
|
|
9123
|
-
...this.currentOnboardingState && {
|
|
9124
|
-
onboardingState: this.currentOnboardingState
|
|
9125
|
-
}
|
|
9126
|
-
};
|
|
9127
|
-
await this.runSingleTurn(combinedCommand, `background-${Date.now()}`);
|
|
9273
|
+
if (this.isDrainBarrier(head)) {
|
|
9274
|
+
const item = this.queue.shift();
|
|
9275
|
+
const rid = item.command.requestId ?? `user-${Date.now()}`;
|
|
9276
|
+
await this.runSingleTurn(item.command, rid, false, true);
|
|
9128
9277
|
continue;
|
|
9129
9278
|
}
|
|
9130
|
-
|
|
9131
|
-
|
|
9279
|
+
let n = 1;
|
|
9280
|
+
let batchOb = head.command.onboardingState;
|
|
9281
|
+
for (; ; n++) {
|
|
9282
|
+
const it = this.queue.peekAt(n);
|
|
9283
|
+
if (!it || it.source === "chain" || this.isDrainBarrier(it)) {
|
|
9284
|
+
break;
|
|
9285
|
+
}
|
|
9286
|
+
const ob = it.command.onboardingState;
|
|
9287
|
+
if (ob !== void 0 && batchOb !== void 0 && ob !== batchOb) {
|
|
9288
|
+
break;
|
|
9289
|
+
}
|
|
9290
|
+
if (ob !== void 0 && batchOb === void 0) {
|
|
9291
|
+
batchOb = ob;
|
|
9292
|
+
}
|
|
9293
|
+
}
|
|
9294
|
+
const batch = this.queue.shiftMany(n);
|
|
9295
|
+
await this.runMergedTurn(batch);
|
|
9132
9296
|
}
|
|
9133
9297
|
}
|
|
9134
9298
|
/**
|
|
@@ -9184,9 +9348,14 @@ var HeadlessSession = class {
|
|
|
9184
9348
|
* Cancel the running turn and flush the follow-ups that belonged to it
|
|
9185
9349
|
* (`chain`/`background`), while preserving `source: 'user'` items — those are
|
|
9186
9350
|
* independent user intent, not tied to the aborted run. The preserved user
|
|
9187
|
-
* messages run next: `
|
|
9351
|
+
* messages run next: `executeTurn` swallows the abort, so `handleMessage`
|
|
9188
9352
|
* falls through to `drainQueueLoop` with `running` still held. Returns the
|
|
9189
9353
|
* flushed items (for the cancel command's resume/discard UX).
|
|
9354
|
+
*
|
|
9355
|
+
* Messages already absorbed into the in-flight merged turn are NOT
|
|
9356
|
+
* preserved — they were delivered into the turn that's being cancelled and
|
|
9357
|
+
* each gets a `{cancelled, absorbed:true}` terminal. Only items still
|
|
9358
|
+
* sitting in the queue survive.
|
|
9190
9359
|
*/
|
|
9191
9360
|
handleCancel() {
|
|
9192
9361
|
if (this.currentAbort) {
|
package/dist/index.js
CHANGED
|
@@ -2195,13 +2195,17 @@ var init_surfaces = __esm({
|
|
|
2195
2195
|
"gemini-3.1-pro",
|
|
2196
2196
|
"gemini-3-flash",
|
|
2197
2197
|
"gemini-3.5-flash",
|
|
2198
|
+
"gemini-3.7-flash",
|
|
2198
2199
|
"grok-build-0.1",
|
|
2199
2200
|
"grok-4.5",
|
|
2201
|
+
"grok-4.6",
|
|
2200
2202
|
"glm-5.2",
|
|
2201
2203
|
"muse-spark-1.1",
|
|
2202
2204
|
"kimi-k2-7-code",
|
|
2203
2205
|
"kimi-k3",
|
|
2204
|
-
"deepseek-v4-flash-0731"
|
|
2206
|
+
"deepseek-v4-flash-0731",
|
|
2207
|
+
"qwen3.8-2.4t-a95b-deepinfra",
|
|
2208
|
+
"minimax-m3"
|
|
2205
2209
|
]
|
|
2206
2210
|
// vision: undefined — unconstrained
|
|
2207
2211
|
// image_generation: undefined — unconstrained
|
|
@@ -8091,9 +8095,7 @@ function createAgentState() {
|
|
|
8091
8095
|
async function runTurn(params) {
|
|
8092
8096
|
const {
|
|
8093
8097
|
state,
|
|
8094
|
-
|
|
8095
|
-
attachments,
|
|
8096
|
-
attachmentHeader,
|
|
8098
|
+
entries,
|
|
8097
8099
|
apiConfig,
|
|
8098
8100
|
system,
|
|
8099
8101
|
model,
|
|
@@ -8102,7 +8104,6 @@ async function runTurn(params) {
|
|
|
8102
8104
|
signal,
|
|
8103
8105
|
onEvent,
|
|
8104
8106
|
resolveExternalTool,
|
|
8105
|
-
hidden,
|
|
8106
8107
|
requestId,
|
|
8107
8108
|
toolRegistry,
|
|
8108
8109
|
onBackgroundComplete
|
|
@@ -8112,45 +8113,54 @@ async function runTurn(params) {
|
|
|
8112
8113
|
const baseline = resolveModel("parent", state.models, model);
|
|
8113
8114
|
const parentModel = buildModelOverride ?? baseline;
|
|
8114
8115
|
const modelOverride = buildModelOverride && buildModelOverride !== baseline ? { from: baseline } : void 0;
|
|
8116
|
+
const totalAttachments = entries.reduce(
|
|
8117
|
+
(n, e) => n + (e.attachments?.length ?? 0),
|
|
8118
|
+
0
|
|
8119
|
+
);
|
|
8115
8120
|
log13.info("Turn started", {
|
|
8116
8121
|
requestId,
|
|
8117
8122
|
model,
|
|
8118
8123
|
buildModel: buildModelOverride,
|
|
8119
8124
|
toolCount: tools2.length,
|
|
8120
|
-
...
|
|
8121
|
-
|
|
8122
|
-
}
|
|
8125
|
+
...entries.length > 1 && { entryCount: entries.length },
|
|
8126
|
+
...totalAttachments > 0 && { attachmentCount: totalAttachments }
|
|
8123
8127
|
});
|
|
8124
8128
|
onEvent({
|
|
8125
8129
|
type: "turn_started",
|
|
8126
8130
|
model: parentModel,
|
|
8127
8131
|
...modelOverride && { modelOverride }
|
|
8128
8132
|
});
|
|
8129
|
-
const
|
|
8130
|
-
|
|
8131
|
-
|
|
8133
|
+
const keptEntries = entries.filter(
|
|
8134
|
+
(e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
|
|
8135
|
+
);
|
|
8136
|
+
if (keptEntries.length === 0) {
|
|
8132
8137
|
onEvent({ type: "error", error: "Empty message" });
|
|
8133
8138
|
return;
|
|
8134
8139
|
}
|
|
8135
|
-
const
|
|
8136
|
-
|
|
8137
|
-
userMsg
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8140
|
+
for (const entry of keptEntries) {
|
|
8141
|
+
const hasAttachments = (entry.attachments?.length ?? 0) > 0;
|
|
8142
|
+
const userMsg = { role: "user", content: entry.text };
|
|
8143
|
+
if (entry.hidden) {
|
|
8144
|
+
userMsg.hidden = true;
|
|
8145
|
+
}
|
|
8146
|
+
if (hasAttachments) {
|
|
8147
|
+
userMsg.attachments = entry.attachments;
|
|
8148
|
+
}
|
|
8149
|
+
if (entry.attachmentHeader) {
|
|
8150
|
+
userMsg.attachmentHeader = entry.attachmentHeader;
|
|
8151
|
+
}
|
|
8152
|
+
state.messages.push(userMsg);
|
|
8153
|
+
onEvent({
|
|
8154
|
+
type: "user_message",
|
|
8155
|
+
text: entry.text,
|
|
8156
|
+
hidden: entry.hidden || void 0,
|
|
8157
|
+
// Include attachments so the live event can render a queued voice/image/file
|
|
8158
|
+
// bubble; a voice message has empty text and the transcript lives here.
|
|
8159
|
+
...hasAttachments && { attachments: entry.attachments },
|
|
8160
|
+
...entry.requestId && { requestId: entry.requestId },
|
|
8161
|
+
...entry.queued && { queued: true }
|
|
8162
|
+
});
|
|
8144
8163
|
}
|
|
8145
|
-
state.messages.push(userMsg);
|
|
8146
|
-
onEvent({
|
|
8147
|
-
type: "user_message",
|
|
8148
|
-
text: userMessage,
|
|
8149
|
-
hidden: hidden || void 0,
|
|
8150
|
-
// Include attachments so the live event can render a queued voice/image/file
|
|
8151
|
-
// bubble; a voice message has empty text and the transcript lives here.
|
|
8152
|
-
...hasAttachments && { attachments }
|
|
8153
|
-
});
|
|
8154
8164
|
const isFirstMessage = state.messages.filter((m) => m.role === "user").length === 1;
|
|
8155
8165
|
const STATUS_EXCLUDED_TOOLS = /* @__PURE__ */ new Set([
|
|
8156
8166
|
"setProjectOnboardingState",
|
|
@@ -8219,11 +8229,13 @@ async function runTurn(params) {
|
|
|
8219
8229
|
if (onboardingState && onboardingState !== "onboardingFinished") {
|
|
8220
8230
|
parts.push(`Build phase: ${onboardingState}`);
|
|
8221
8231
|
}
|
|
8222
|
-
const
|
|
8223
|
-
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8232
|
+
for (const entry of keptEntries) {
|
|
8233
|
+
const automated = parseSentinel(entry.text);
|
|
8234
|
+
if (automated) {
|
|
8235
|
+
parts.push(`Automated action: ${automated.name}`);
|
|
8236
|
+
} else if (entry.text) {
|
|
8237
|
+
parts.push(`User request: ${entry.text.slice(-500)}`);
|
|
8238
|
+
}
|
|
8227
8239
|
}
|
|
8228
8240
|
return parts.join("\n");
|
|
8229
8241
|
},
|
|
@@ -9144,25 +9156,24 @@ async function persistAttachments(attachments) {
|
|
|
9144
9156
|
images: settled.filter((s) => s.isImage).map((s) => s.result)
|
|
9145
9157
|
};
|
|
9146
9158
|
}
|
|
9147
|
-
function buildUploadHeader(
|
|
9148
|
-
const
|
|
9149
|
-
|
|
9159
|
+
function buildUploadHeader(documents, images) {
|
|
9160
|
+
const entries = [
|
|
9161
|
+
...documents.filter((r) => r !== null).map((r) => ({ ...r, isImage: false })),
|
|
9162
|
+
...images.filter((r) => r !== null).map((r) => ({ ...r, isImage: true }))
|
|
9163
|
+
];
|
|
9164
|
+
if (entries.length === 0) {
|
|
9150
9165
|
return "";
|
|
9151
9166
|
}
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
const
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
}
|
|
9158
|
-
return parts.join(" \u2014 ") + "]";
|
|
9167
|
+
const detail = (e) => e.extractedTextPath ? `extracted text: ${e.extractedTextPath}` : e.isImage ? null : "no extracted text \u2014 raw file only";
|
|
9168
|
+
if (entries.length === 1) {
|
|
9169
|
+
const e = entries[0];
|
|
9170
|
+
const extra = detail(e);
|
|
9171
|
+
return `[Uploaded file: ${e.localPath}${extra ? ` \u2014 ${extra}` : ""}]`;
|
|
9159
9172
|
}
|
|
9160
|
-
const lines =
|
|
9161
|
-
const
|
|
9162
|
-
|
|
9163
|
-
|
|
9164
|
-
}
|
|
9165
|
-
return parts.join("\n");
|
|
9173
|
+
const lines = entries.map((e) => {
|
|
9174
|
+
const extra = detail(e);
|
|
9175
|
+
return `- ${e.localPath}${extra ? `
|
|
9176
|
+
${extra}` : ""}`;
|
|
9166
9177
|
});
|
|
9167
9178
|
return `[Uploaded files]
|
|
9168
9179
|
${lines.join("\n")}`;
|
|
@@ -9275,6 +9286,17 @@ var init_messageQueue = __esm({
|
|
|
9275
9286
|
}
|
|
9276
9287
|
return item;
|
|
9277
9288
|
}
|
|
9289
|
+
/** Remove and return the first `n` items; fires onChange once. */
|
|
9290
|
+
shiftMany(n) {
|
|
9291
|
+
if (n <= 0) {
|
|
9292
|
+
return [];
|
|
9293
|
+
}
|
|
9294
|
+
const items = this.items.splice(0, n);
|
|
9295
|
+
if (items.length > 0) {
|
|
9296
|
+
this.onChange?.();
|
|
9297
|
+
}
|
|
9298
|
+
return items;
|
|
9299
|
+
}
|
|
9278
9300
|
/** Remove and return all queued items. */
|
|
9279
9301
|
drain() {
|
|
9280
9302
|
if (this.items.length === 0) {
|
|
@@ -9310,6 +9332,10 @@ var init_messageQueue = __esm({
|
|
|
9310
9332
|
peek() {
|
|
9311
9333
|
return this.items[0];
|
|
9312
9334
|
}
|
|
9335
|
+
/** Return the item at index `i` without removing it. */
|
|
9336
|
+
peekAt(i) {
|
|
9337
|
+
return this.items[i];
|
|
9338
|
+
}
|
|
9313
9339
|
get length() {
|
|
9314
9340
|
return this.items.length;
|
|
9315
9341
|
}
|
|
@@ -9427,8 +9453,12 @@ var init_headless = __esm({
|
|
|
9427
9453
|
currentAbort = null;
|
|
9428
9454
|
/** RequestId of the in-flight message command — injected into streamed events. */
|
|
9429
9455
|
currentRequestId;
|
|
9430
|
-
/** Guard: track whether terminal `completed` was already sent so we emit
|
|
9456
|
+
/** Guard: track whether terminal `completed` was already sent so we emit
|
|
9457
|
+
* exactly one per requestId. */
|
|
9431
9458
|
completedEmitted = false;
|
|
9459
|
+
/** Outcome of the current turn's primary `completed` — read after the turn
|
|
9460
|
+
* to stamp the same success/error onto absorbed requestIds' terminals. */
|
|
9461
|
+
lastCompleted = null;
|
|
9432
9462
|
turnStart = 0;
|
|
9433
9463
|
/**
|
|
9434
9464
|
* Onboarding state of the currently-running turn. Captured at runSingleTurn
|
|
@@ -9564,6 +9594,15 @@ var init_headless = __esm({
|
|
|
9564
9594
|
emitCompleted(rid, data) {
|
|
9565
9595
|
this.emit("completed", { ...data }, rid);
|
|
9566
9596
|
this.completedEmitted = true;
|
|
9597
|
+
this.lastCompleted = {
|
|
9598
|
+
success: data.success === true,
|
|
9599
|
+
...typeof data.error === "string" && { error: data.error }
|
|
9600
|
+
};
|
|
9601
|
+
}
|
|
9602
|
+
/** Outcome of the turn's primary `completed`, for stamping onto absorbed
|
|
9603
|
+
* requestIds' terminals. Falls back to failure if none was emitted. */
|
|
9604
|
+
primaryOutcome() {
|
|
9605
|
+
return this.lastCompleted ?? { success: false };
|
|
9567
9606
|
}
|
|
9568
9607
|
/** Dispatch a simple (non-streaming) command: call handler, emit response + completed. */
|
|
9569
9608
|
dispatchSimple(requestId, eventName, handler) {
|
|
@@ -9722,9 +9761,14 @@ var init_headless = __esm({
|
|
|
9722
9761
|
text: e.text,
|
|
9723
9762
|
// Forward attachments so queued voice/image/file sends render live;
|
|
9724
9763
|
// otherwise the bubble is blank until a get_history refresh.
|
|
9725
|
-
...e.attachments && { attachments: e.attachments }
|
|
9764
|
+
...e.attachments && { attachments: e.attachments },
|
|
9765
|
+
// Queue-delivered entries are flagged so the frontend renders the
|
|
9766
|
+
// echo (idle sends are rendered optimistically instead).
|
|
9767
|
+
...e.queued && { queued: true }
|
|
9726
9768
|
},
|
|
9727
|
-
|
|
9769
|
+
// A merged turn emits one user_message per absorbed entry — each
|
|
9770
|
+
// carries its own original requestId, not the turn's.
|
|
9771
|
+
e.requestId ?? rid
|
|
9728
9772
|
);
|
|
9729
9773
|
return;
|
|
9730
9774
|
// Terminal events — translate to `completed`.
|
|
@@ -9747,11 +9791,9 @@ var init_headless = __esm({
|
|
|
9747
9791
|
durationMs: Date.now() - this.turnStart
|
|
9748
9792
|
});
|
|
9749
9793
|
return;
|
|
9750
|
-
case "turn_cancelled":
|
|
9751
|
-
this.
|
|
9752
|
-
this.completedEmitted = true;
|
|
9794
|
+
case "turn_cancelled":
|
|
9795
|
+
this.emitCompleted(rid, { success: false, error: "cancelled" });
|
|
9753
9796
|
return;
|
|
9754
|
-
}
|
|
9755
9797
|
// Streaming events — forward with requestId
|
|
9756
9798
|
case "text":
|
|
9757
9799
|
this.emit(
|
|
@@ -9871,17 +9913,34 @@ var init_headless = __esm({
|
|
|
9871
9913
|
// Message command handler (long-running / streaming)
|
|
9872
9914
|
//////////////////////////////////////////////////////////////////////////////
|
|
9873
9915
|
/**
|
|
9874
|
-
*
|
|
9875
|
-
*
|
|
9876
|
-
*
|
|
9877
|
-
*
|
|
9916
|
+
* Persist one entry's non-voice uploads to disk and build its header. The
|
|
9917
|
+
* header tells the LLM where to read each file; it's kept separate so it
|
|
9918
|
+
* gets injected at API-send time and never persisted into the user's chat
|
|
9919
|
+
* content (which would leak into history restore on the frontend).
|
|
9920
|
+
*
|
|
9921
|
+
* Must be awaited sequentially across entries — persistAttachments'
|
|
9922
|
+
* filename de-dup set is per-call, so parallel calls race on names.
|
|
9878
9923
|
*/
|
|
9879
|
-
async
|
|
9880
|
-
|
|
9881
|
-
|
|
9882
|
-
|
|
9883
|
-
|
|
9884
|
-
|
|
9924
|
+
async persistEntryAttachments(attachments) {
|
|
9925
|
+
if (!attachments?.some((a) => !a.isVoice)) {
|
|
9926
|
+
return void 0;
|
|
9927
|
+
}
|
|
9928
|
+
try {
|
|
9929
|
+
const { documents, images } = await persistAttachments(attachments);
|
|
9930
|
+
return buildUploadHeader(documents, images) || void 0;
|
|
9931
|
+
} catch (err) {
|
|
9932
|
+
log16.warn("Attachment persistence failed", { error: err.message });
|
|
9933
|
+
return void 0;
|
|
9934
|
+
}
|
|
9935
|
+
}
|
|
9936
|
+
/**
|
|
9937
|
+
* Run one turn for a single command (without acquiring the `running` lock).
|
|
9938
|
+
* Owns the per-command machinery: @@automated:: action resolution, plan-file
|
|
9939
|
+
* and buildModel side effects, and chain expansion — which is why
|
|
9940
|
+
* sentinel-bearing commands always come through here, one turn each, never
|
|
9941
|
+
* merged. The turn itself runs in executeTurn.
|
|
9942
|
+
*/
|
|
9943
|
+
async runSingleTurn(parsed, requestId, fromChain = false, queued = false) {
|
|
9885
9944
|
const attachments = parsed.attachments;
|
|
9886
9945
|
if (attachments?.length) {
|
|
9887
9946
|
log16.info("Message has attachments", {
|
|
@@ -9890,19 +9949,7 @@ var init_headless = __esm({
|
|
|
9890
9949
|
});
|
|
9891
9950
|
}
|
|
9892
9951
|
let userMessage = parsed.text ?? "";
|
|
9893
|
-
|
|
9894
|
-
if (attachments?.some((a) => !a.isVoice)) {
|
|
9895
|
-
try {
|
|
9896
|
-
const { documents, images } = await persistAttachments(attachments);
|
|
9897
|
-
const all = [...documents, ...images];
|
|
9898
|
-
const header = buildUploadHeader(all);
|
|
9899
|
-
if (header) {
|
|
9900
|
-
attachmentHeader = header;
|
|
9901
|
-
}
|
|
9902
|
-
} catch (err) {
|
|
9903
|
-
log16.warn("Attachment persistence failed", { error: err.message });
|
|
9904
|
-
}
|
|
9905
|
-
}
|
|
9952
|
+
const attachmentHeader = await this.persistEntryAttachments(attachments);
|
|
9906
9953
|
let resolved = null;
|
|
9907
9954
|
try {
|
|
9908
9955
|
resolved = resolveAction(userMessage);
|
|
@@ -9939,22 +9986,108 @@ var init_headless = __esm({
|
|
|
9939
9986
|
});
|
|
9940
9987
|
}
|
|
9941
9988
|
}
|
|
9989
|
+
await this.executeTurn({
|
|
9990
|
+
entries: [
|
|
9991
|
+
{
|
|
9992
|
+
text: userMessage,
|
|
9993
|
+
attachments,
|
|
9994
|
+
attachmentHeader,
|
|
9995
|
+
hidden: isHidden || void 0,
|
|
9996
|
+
requestId,
|
|
9997
|
+
queued: queued || void 0
|
|
9998
|
+
}
|
|
9999
|
+
],
|
|
10000
|
+
requestId,
|
|
10001
|
+
absorbedRids: [],
|
|
10002
|
+
onboardingState,
|
|
10003
|
+
system,
|
|
10004
|
+
buildModel
|
|
10005
|
+
});
|
|
10006
|
+
}
|
|
10007
|
+
/**
|
|
10008
|
+
* Run a mailbox batch — contiguous queued user + background items — as one
|
|
10009
|
+
* merged turn. Every item becomes its own history entry and user_message
|
|
10010
|
+
* event (own requestId, attachments, hidden flag); adjacent background
|
|
10011
|
+
* items fold into a single background_results entry so the LLM sees one
|
|
10012
|
+
* combined block. No action-sentinel machinery here: batch construction
|
|
10013
|
+
* guarantees none (sentinel-bearing user items are drain barriers that run
|
|
10014
|
+
* alone via runSingleTurn, and background_results is a NON_ACTION sentinel
|
|
10015
|
+
* with no side effects).
|
|
10016
|
+
*/
|
|
10017
|
+
async runMergedTurn(batch) {
|
|
10018
|
+
const primaryRid = batch[0].command.requestId ?? (batch.every((b) => b.source === "background") ? `background-${Date.now()}` : `merged-${Date.now()}`);
|
|
10019
|
+
const absorbedRids = batch.slice(1).map((b) => b.command.requestId).filter((rid) => typeof rid === "string");
|
|
10020
|
+
const entryList = [];
|
|
10021
|
+
for (const item of batch) {
|
|
10022
|
+
const text = item.command.text ?? "";
|
|
10023
|
+
const prev = entryList[entryList.length - 1];
|
|
10024
|
+
if (item.source === "background" && prev?.background) {
|
|
10025
|
+
prev.entry.text = mergeBackgroundResultsMessages([
|
|
10026
|
+
prev.entry.text,
|
|
10027
|
+
text
|
|
10028
|
+
]);
|
|
10029
|
+
continue;
|
|
10030
|
+
}
|
|
10031
|
+
entryList.push({
|
|
10032
|
+
background: item.source === "background",
|
|
10033
|
+
entry: {
|
|
10034
|
+
text,
|
|
10035
|
+
attachments: item.command.attachments,
|
|
10036
|
+
hidden: !!item.command.hidden || void 0,
|
|
10037
|
+
// Background items carry no requestId — their user_message falls
|
|
10038
|
+
// back to the turn's primary rid on the wire.
|
|
10039
|
+
requestId: item.command.requestId,
|
|
10040
|
+
queued: true
|
|
10041
|
+
}
|
|
10042
|
+
});
|
|
10043
|
+
}
|
|
10044
|
+
const entries = entryList.map((e) => e.entry);
|
|
10045
|
+
for (const entry of entries) {
|
|
10046
|
+
entry.attachmentHeader = await this.persistEntryAttachments(
|
|
10047
|
+
entry.attachments
|
|
10048
|
+
);
|
|
10049
|
+
}
|
|
10050
|
+
const onboardingState = batch.find((b) => b.command.onboardingState !== void 0)?.command.onboardingState ?? this.currentOnboardingState ?? "onboardingFinished";
|
|
10051
|
+
this.currentOnboardingState = onboardingState;
|
|
10052
|
+
const viewContext = [...batch].reverse().find((b) => b.command.viewContext !== void 0)?.command.viewContext;
|
|
10053
|
+
await this.executeTurn({
|
|
10054
|
+
entries,
|
|
10055
|
+
requestId: primaryRid,
|
|
10056
|
+
absorbedRids,
|
|
10057
|
+
onboardingState,
|
|
10058
|
+
system: buildSystemPrompt(onboardingState, viewContext)
|
|
10059
|
+
});
|
|
10060
|
+
}
|
|
10061
|
+
/**
|
|
10062
|
+
* Run one agent turn over the given entries (without acquiring the
|
|
10063
|
+
* `running` lock). Owns the turn-generic lifecycle: request bookkeeping,
|
|
10064
|
+
* the forced-compaction gate, runTurn error handling, and terminal
|
|
10065
|
+
* `completed` events — the primary requestId's completed first, then one
|
|
10066
|
+
* `{absorbed: true}` completed per other absorbed requestId with the same
|
|
10067
|
+
* outcome, on every exit path (done, cancel, error, unexpected), so every
|
|
10068
|
+
* queued message's caller resolves.
|
|
10069
|
+
*/
|
|
10070
|
+
async executeTurn(params) {
|
|
10071
|
+
const { entries, requestId, absorbedRids, onboardingState, system } = params;
|
|
10072
|
+
this.currentRequestId = requestId;
|
|
10073
|
+
this.currentAbort = new AbortController();
|
|
10074
|
+
this.completedEmitted = false;
|
|
10075
|
+
this.lastCompleted = null;
|
|
10076
|
+
this.turnStart = Date.now();
|
|
10077
|
+
await this.runForcedCompactionIfNeeded(requestId);
|
|
9942
10078
|
try {
|
|
9943
10079
|
await runTurn({
|
|
9944
10080
|
state: this.state,
|
|
9945
|
-
|
|
9946
|
-
attachments,
|
|
9947
|
-
attachmentHeader,
|
|
10081
|
+
entries,
|
|
9948
10082
|
apiConfig: this.config,
|
|
9949
10083
|
system,
|
|
9950
10084
|
model: this.opts.model,
|
|
9951
|
-
buildModel,
|
|
10085
|
+
buildModel: params.buildModel,
|
|
9952
10086
|
onboardingState,
|
|
9953
10087
|
requestId,
|
|
9954
10088
|
signal: this.currentAbort.signal,
|
|
9955
10089
|
onEvent: this.onEvent,
|
|
9956
10090
|
resolveExternalTool: this.resolveExternalTool,
|
|
9957
|
-
hidden: isHidden,
|
|
9958
10091
|
toolRegistry: this.toolRegistry,
|
|
9959
10092
|
onBackgroundComplete: this.onBackgroundComplete
|
|
9960
10093
|
});
|
|
@@ -9982,6 +10115,18 @@ var init_headless = __esm({
|
|
|
9982
10115
|
error: err.message
|
|
9983
10116
|
});
|
|
9984
10117
|
}
|
|
10118
|
+
const outcome = this.primaryOutcome();
|
|
10119
|
+
for (const rid of absorbedRids) {
|
|
10120
|
+
this.emit(
|
|
10121
|
+
"completed",
|
|
10122
|
+
{
|
|
10123
|
+
success: outcome.success,
|
|
10124
|
+
...outcome.error && { error: outcome.error },
|
|
10125
|
+
absorbed: true
|
|
10126
|
+
},
|
|
10127
|
+
rid
|
|
10128
|
+
);
|
|
10129
|
+
}
|
|
9985
10130
|
applyPendingSummaries(this.state);
|
|
9986
10131
|
this.applyPendingBlockUpdates();
|
|
9987
10132
|
}
|
|
@@ -10008,42 +10153,61 @@ var init_headless = __esm({
|
|
|
10008
10153
|
this.running = false;
|
|
10009
10154
|
}
|
|
10010
10155
|
}
|
|
10156
|
+
/**
|
|
10157
|
+
* True for queued user items whose text is an @@automated:: action message.
|
|
10158
|
+
* These key per-item raw-text side effects in runSingleTurn (resolveAction,
|
|
10159
|
+
* plan file, buildModel, chain expansion) — they always run alone, one turn
|
|
10160
|
+
* each. Background items are sentinel-formatted too but background_results
|
|
10161
|
+
* is NON_ACTION and side-effect-free, so they merge freely.
|
|
10162
|
+
*/
|
|
10163
|
+
isDrainBarrier(item) {
|
|
10164
|
+
return item.source === "user" && isAutomatedMessage(item.command.text ?? "");
|
|
10165
|
+
}
|
|
10011
10166
|
/**
|
|
10012
10167
|
* Drain the queue in strict FIFO order. Caller must hold `running = true`.
|
|
10013
10168
|
* User messages arriving during the drain will be enqueued behind current items.
|
|
10014
10169
|
*
|
|
10015
|
-
*
|
|
10016
|
-
*
|
|
10017
|
-
*
|
|
10170
|
+
* The queue serves two purposes with opposite delivery semantics:
|
|
10171
|
+
* - Sequencer: chain steps and sentinel-bearing user items are pipeline
|
|
10172
|
+
* stages — one item, one turn, nothing merged in.
|
|
10173
|
+
* - Mailbox: plain user messages and background results are accumulated
|
|
10174
|
+
* context and intent — everything contiguous flushes together into ONE
|
|
10175
|
+
* merged turn, so the model reconciles all of it at once instead of
|
|
10176
|
+
* burning a full turn per item (and possibly executing instructions a
|
|
10177
|
+
* later queued message already amended).
|
|
10018
10178
|
*/
|
|
10019
10179
|
async drainQueueLoop() {
|
|
10020
|
-
while (
|
|
10021
|
-
const
|
|
10022
|
-
if (
|
|
10023
|
-
|
|
10180
|
+
while (this.queue.length > 0) {
|
|
10181
|
+
const head = this.queue.peek();
|
|
10182
|
+
if (head.source === "chain") {
|
|
10183
|
+
const item = this.queue.shift();
|
|
10184
|
+
const rid = item.command.requestId ?? `chain-${Date.now()}`;
|
|
10185
|
+
await this.runSingleTurn(item.command, rid, true);
|
|
10186
|
+
continue;
|
|
10024
10187
|
}
|
|
10025
|
-
if (
|
|
10026
|
-
const
|
|
10027
|
-
|
|
10028
|
-
|
|
10029
|
-
if (more) {
|
|
10030
|
-
batch.push(more);
|
|
10031
|
-
}
|
|
10032
|
-
}
|
|
10033
|
-
const combinedCommand = {
|
|
10034
|
-
action: "message",
|
|
10035
|
-
text: mergeBackgroundResultsMessages(
|
|
10036
|
-
batch.map((b) => b.command.text ?? "")
|
|
10037
|
-
),
|
|
10038
|
-
...this.currentOnboardingState && {
|
|
10039
|
-
onboardingState: this.currentOnboardingState
|
|
10040
|
-
}
|
|
10041
|
-
};
|
|
10042
|
-
await this.runSingleTurn(combinedCommand, `background-${Date.now()}`);
|
|
10188
|
+
if (this.isDrainBarrier(head)) {
|
|
10189
|
+
const item = this.queue.shift();
|
|
10190
|
+
const rid = item.command.requestId ?? `user-${Date.now()}`;
|
|
10191
|
+
await this.runSingleTurn(item.command, rid, false, true);
|
|
10043
10192
|
continue;
|
|
10044
10193
|
}
|
|
10045
|
-
|
|
10046
|
-
|
|
10194
|
+
let n = 1;
|
|
10195
|
+
let batchOb = head.command.onboardingState;
|
|
10196
|
+
for (; ; n++) {
|
|
10197
|
+
const it = this.queue.peekAt(n);
|
|
10198
|
+
if (!it || it.source === "chain" || this.isDrainBarrier(it)) {
|
|
10199
|
+
break;
|
|
10200
|
+
}
|
|
10201
|
+
const ob = it.command.onboardingState;
|
|
10202
|
+
if (ob !== void 0 && batchOb !== void 0 && ob !== batchOb) {
|
|
10203
|
+
break;
|
|
10204
|
+
}
|
|
10205
|
+
if (ob !== void 0 && batchOb === void 0) {
|
|
10206
|
+
batchOb = ob;
|
|
10207
|
+
}
|
|
10208
|
+
}
|
|
10209
|
+
const batch = this.queue.shiftMany(n);
|
|
10210
|
+
await this.runMergedTurn(batch);
|
|
10047
10211
|
}
|
|
10048
10212
|
}
|
|
10049
10213
|
/**
|
|
@@ -10099,9 +10263,14 @@ var init_headless = __esm({
|
|
|
10099
10263
|
* Cancel the running turn and flush the follow-ups that belonged to it
|
|
10100
10264
|
* (`chain`/`background`), while preserving `source: 'user'` items — those are
|
|
10101
10265
|
* independent user intent, not tied to the aborted run. The preserved user
|
|
10102
|
-
* messages run next: `
|
|
10266
|
+
* messages run next: `executeTurn` swallows the abort, so `handleMessage`
|
|
10103
10267
|
* falls through to `drainQueueLoop` with `running` still held. Returns the
|
|
10104
10268
|
* flushed items (for the cancel command's resume/discard UX).
|
|
10269
|
+
*
|
|
10270
|
+
* Messages already absorbed into the in-flight merged turn are NOT
|
|
10271
|
+
* preserved — they were delivered into the turn that's being cancelled and
|
|
10272
|
+
* each gets a `{cancelled, absorbed:true}` terminal. Only items still
|
|
10273
|
+
* sitting in the queue survive.
|
|
10105
10274
|
*/
|
|
10106
10275
|
handleCancel() {
|
|
10107
10276
|
if (this.currentAbort) {
|
|
@@ -10528,7 +10697,7 @@ function App({ apiConfig, model }) {
|
|
|
10528
10697
|
try {
|
|
10529
10698
|
await runTurn({
|
|
10530
10699
|
state: agentState,
|
|
10531
|
-
|
|
10700
|
+
entries: [{ text: message }],
|
|
10532
10701
|
apiConfig,
|
|
10533
10702
|
system,
|
|
10534
10703
|
model,
|
|
@@ -28,7 +28,7 @@ The user can already see your tool calls, so most of your work is visible withou
|
|
|
28
28
|
Skip the rest: narrating what you're about to do, restating what the user asked, explaining tool calls they can already see.
|
|
29
29
|
|
|
30
30
|
### User attachments
|
|
31
|
-
When a user uploads a file (PDF, Word doc, image, etc.), it is automatically saved to `src/.user-uploads/` in the project directory. The message includes the local file path, and for documents with extractable text, a `.txt` sidecar with the extracted content
|
|
31
|
+
When a user uploads a file (PDF, Word doc, image, etc.), it is automatically saved to `src/.user-uploads/` in the project directory. The message includes the local file path, and for documents with extractable text, a `.txt` sidecar with the extracted content you can read with `readFile`. A document marked "no extracted text — raw file only" has no sidecar: parse the raw file yourself, and if it is genuinely unreadable, tell the user what happened. Pass the file path itself to tools that take an image — `screenshot`, and the design expert's `analyzeImage` / `analyzeDesign` / `editImages` — and they host the file and hand back a URL you can reuse or embed in a spec. If a raw file from `src/` needs to be served by the web interface, copy it to `dist/interfaces/web/public/`. These files persist across the conversation — they survive compaction and session restarts. Do not ask the user to re-upload a document that has already been saved. Voice messages are not saved to disk — their transcripts appear inline in the message.
|
|
32
32
|
|
|
33
33
|
### Automated messages
|
|
34
34
|
You will occasionally receive automated messages prefixed with `@@automated_message@@` - these are triggered by things like background agents returning their work, or by the user clicking a button in the UI (e.g., the user might click a "Build Feature" button in the product roadmap UI, and you will receive a message detailing what they want to build). You will be able to see these messages in your chat history but the user will not see them, so acknowledge them appropriately and then perform the requested work.
|
|
@@ -71,6 +71,7 @@ Result text here...
|
|
|
71
71
|
When you receive background results:
|
|
72
72
|
- Acknowledge them briefly to the user if relevant to what they're doing (e.g., "By the way, the designer finished those icons..." or "Looks like the roadmap is ready...")
|
|
73
73
|
- Don't interrupt the user's flow with a lengthy summary — they can see the background work in the UI
|
|
74
|
+
- Background results may arrive in the same turn as new user messages. When they do, the user's messages are the priority — fold the results in where they matter and get on with what the user asked.
|
|
74
75
|
|
|
75
76
|
#### When You Are Allowed to Background
|
|
76
77
|
|