@oh-my-pi/pi-coding-agent 16.0.2 → 16.0.4
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/CHANGELOG.md +53 -0
- package/README.md +0 -1
- package/dist/cli.js +580 -359
- package/dist/types/advisor/advise-tool.d.ts +30 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/commands/install.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +8 -0
- package/dist/types/config/settings-schema.d.ts +1 -11
- package/dist/types/edit/file-snapshot-store.d.ts +2 -0
- package/dist/types/eval/js/shared/runtime.d.ts +1 -0
- package/dist/types/eval/js/worker-core.d.ts +1 -0
- package/dist/types/extensibility/extensions/loader.d.ts +2 -2
- package/dist/types/goals/runtime.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +3 -0
- package/dist/types/modes/components/custom-editor.d.ts +14 -4
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +3 -2
- package/dist/types/modes/theme/mermaid-cache.d.ts +18 -1
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +16 -1
- package/dist/types/sdk.d.ts +8 -0
- package/dist/types/session/agent-session.d.ts +20 -8
- package/dist/types/session/session-dump-format.d.ts +8 -2
- package/dist/types/session/session-entries.d.ts +4 -0
- package/dist/types/session/session-history-format.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +22 -0
- package/dist/types/stt/downloader.d.ts +5 -5
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/persisted-revive.d.ts +36 -0
- package/dist/types/tiny/models.d.ts +8 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/index.d.ts +0 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +150 -50
- package/src/advisor/advise-tool.ts +48 -6
- package/src/advisor/runtime.ts +10 -3
- package/src/auto-thinking/classifier.ts +12 -3
- package/src/cli/args.ts +3 -0
- package/src/cli/flag-tables.ts +1 -0
- package/src/cli.ts +2 -2
- package/src/commands/install.ts +3 -3
- package/src/commands/launch.ts +3 -0
- package/src/config/model-resolver.ts +28 -11
- package/src/config/settings-schema.ts +1 -12
- package/src/edit/file-snapshot-store.ts +12 -3
- package/src/eval/agent-bridge.ts +2 -0
- package/src/eval/js/context-manager.ts +2 -1
- package/src/eval/js/shared/runtime.ts +189 -15
- package/src/eval/js/worker-core.ts +19 -0
- package/src/export/html/index.ts +1 -1
- package/src/export/html/tool-views.generated.js +34 -35
- package/src/extensibility/extensions/loader.ts +21 -9
- package/src/goals/runtime.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +82 -84
- package/src/main.ts +26 -4
- package/src/mcp/render.ts +11 -1
- package/src/mcp/tool-bridge.ts +3 -0
- package/src/modes/components/custom-editor.test.ts +63 -18
- package/src/modes/components/custom-editor.ts +63 -15
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/input-controller.ts +15 -9
- package/src/modes/controllers/selector-controller.ts +13 -8
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +4 -2
- package/src/modes/setup-wizard/wizard-overlay.ts +26 -4
- package/src/modes/theme/mermaid-cache.ts +74 -11
- package/src/modes/theme/theme.ts +14 -1
- package/src/modes/types.ts +1 -1
- package/src/prompts/system/system-prompt.md +4 -1
- package/src/registry/agent-lifecycle.ts +60 -8
- package/src/sdk.ts +20 -26
- package/src/session/agent-session.ts +253 -82
- package/src/session/artifacts.ts +19 -1
- package/src/session/session-dump-format.ts +167 -23
- package/src/session/session-entries.ts +4 -0
- package/src/session/session-history-format.ts +37 -3
- package/src/session/session-manager.ts +94 -4
- package/src/slash-commands/builtin-registry.ts +4 -7
- package/src/stt/asr-client.ts +6 -0
- package/src/stt/downloader.ts +13 -6
- package/src/stt/stt-controller.ts +52 -11
- package/src/task/executor.ts +18 -2
- package/src/task/index.ts +2 -2
- package/src/task/persisted-revive.ts +128 -0
- package/src/tiny/models.ts +10 -0
- package/src/tiny/worker.ts +4 -3
- package/src/tools/builtin-names.ts +0 -1
- package/src/tools/index.ts +0 -4
- package/src/tools/output-meta.ts +17 -3
- package/src/tools/read.ts +26 -0
- package/src/utils/title-generator.ts +4 -4
- package/dist/types/tools/render-mermaid.d.ts +0 -38
- package/src/prompts/tools/render-mermaid.md +0 -9
- package/src/tools/render-mermaid.ts +0 -69
|
@@ -126,7 +126,7 @@ import {
|
|
|
126
126
|
AdvisorRuntime,
|
|
127
127
|
type AdvisorSeverity,
|
|
128
128
|
formatAdvisorBatchContent,
|
|
129
|
-
|
|
129
|
+
resolveAdvisorDeliveryChannel,
|
|
130
130
|
} from "../advisor";
|
|
131
131
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
132
132
|
import { classifyDifficulty } from "../auto-thinking/classifier";
|
|
@@ -250,7 +250,7 @@ import {
|
|
|
250
250
|
} from "../tool-discovery/tool-index";
|
|
251
251
|
import { assertEditableFile } from "../tools/auto-generated-guard";
|
|
252
252
|
import type { CheckpointState } from "../tools/checkpoint";
|
|
253
|
-
import { outputMeta } from "../tools/output-meta";
|
|
253
|
+
import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
|
|
254
254
|
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
|
255
255
|
import { isAutoQaEnabled } from "../tools/report-tool-issue";
|
|
256
256
|
import { getLatestTodoPhasesFromEntries, type TodoItem, type TodoPhase } from "../tools/todo";
|
|
@@ -955,6 +955,44 @@ function isAdvisorCard(message: AgentMessage): message is CustomMessage {
|
|
|
955
955
|
return message.role === "custom" && message.customType === "advisor";
|
|
956
956
|
}
|
|
957
957
|
|
|
958
|
+
/**
|
|
959
|
+
* A queued message the user can restore to the editor / pull back as a draft.
|
|
960
|
+
* Only genuinely user-authored messages qualify: plain user turns, or custom
|
|
961
|
+
* messages explicitly attributed to the user (e.g. `/skill` invocations).
|
|
962
|
+
* Agent-authored queued cards — advisor concern/blocker notes, IRC asides,
|
|
963
|
+
* extension notices, hidden goal/plan/budget steers — ride the same
|
|
964
|
+
* steer/follow-up queues but must never be dumped into the editor on Esc/Alt+Up.
|
|
965
|
+
*/
|
|
966
|
+
function isUserQueuedMessage(message: AgentMessage): boolean {
|
|
967
|
+
if (message.role === "user") return true;
|
|
968
|
+
return message.role === "custom" && message.attribution === "user" && message.display !== false;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Custom-message types of the hidden magic-keyword notices that `#createMagicKeywordNotices`
|
|
972
|
+
* enqueues alongside a user prompt. Keep in sync with that method. */
|
|
973
|
+
const MAGIC_KEYWORD_NOTICE_TYPES: ReadonlySet<string> = new Set([
|
|
974
|
+
"ultrathink-notice",
|
|
975
|
+
"orchestrate-notice",
|
|
976
|
+
"workflow-notice",
|
|
977
|
+
]);
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* A hidden, user-attributed companion of a queued user prompt: the magic-keyword
|
|
981
|
+
* notices (`ultrathink`/`orchestrate`/`workflow`) enqueued alongside the user
|
|
982
|
+
* message. They are `attribution: "user"` but `display: false`, so they are not
|
|
983
|
+
* editor-restorable; when the user pulls their prompt back out of the queue these
|
|
984
|
+
* must leave with it rather than linger as stale, companion-less steering. Scoped to
|
|
985
|
+
* the known notice types so an unrelated hidden user custom is never silently dropped.
|
|
986
|
+
*/
|
|
987
|
+
function isHiddenUserCompanion(message: AgentMessage): boolean {
|
|
988
|
+
return (
|
|
989
|
+
message.role === "custom" &&
|
|
990
|
+
message.attribution === "user" &&
|
|
991
|
+
message.display === false &&
|
|
992
|
+
MAGIC_KEYWORD_NOTICE_TYPES.has(message.customType)
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
958
996
|
function queueChipText(message: AgentMessage): string {
|
|
959
997
|
if (message.role === "custom") {
|
|
960
998
|
return readQueueChipText(message.details) ?? queuedTextContent(message) ?? "";
|
|
@@ -1260,7 +1298,70 @@ export class AgentSession {
|
|
|
1260
1298
|
* queue was consumed normally or a new turn already started. */
|
|
1261
1299
|
#drainStrandedQueuedMessages(): void {
|
|
1262
1300
|
if (this.#abortInProgress) return;
|
|
1301
|
+
// A concern steered into a resumed streaming run after a user interrupt can
|
|
1302
|
+
// strand at the turn tail (steered past the loop's final boundary poll). While
|
|
1303
|
+
// that interrupt's suppression is still in effect, reclaim such advisor steers
|
|
1304
|
+
// as visible advice once idle — mirroring abort's #extractQueuedAdvisorCards —
|
|
1305
|
+
// so they neither auto-resume the run the user stopped (a non-empty steer queue
|
|
1306
|
+
// otherwise bypasses the latch in #canAutoContinueForFollowUp) nor linger to
|
|
1307
|
+
// flush at the next prompt. Real user steers/follow-ups are left untouched.
|
|
1308
|
+
if (this.#advisorAutoResumeSuppressed && !this.isStreaming) {
|
|
1309
|
+
for (const card of this.#extractQueuedAdvisorCards()) {
|
|
1310
|
+
this.#preserveAdvisorCard(card);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1263
1313
|
this.#scheduleQueuedMessageDrain();
|
|
1314
|
+
this.#resumeStrandedIrcAsides();
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
/** IRC asides that arrive after the loop's final aside poll — or while an abort skipped that
|
|
1318
|
+
* poll — land in #pendingIrcAsides with no loop left to drain them; the queued-message drain's
|
|
1319
|
+
* gate (agent.hasQueuedMessages()) does not count them. Once idle, wake a turn so the agent
|
|
1320
|
+
* responds to the peer. Skip only when a queued steer/follow-up will itself drive a resume turn
|
|
1321
|
+
* whose aside poll already consumes these (no double-wake). */
|
|
1322
|
+
#resumeStrandedIrcAsides(): void {
|
|
1323
|
+
if (this.#isDisposed || this.isStreaming) return;
|
|
1324
|
+
if (this.#pendingIrcAsides.length === 0) return;
|
|
1325
|
+
if (this.#canAutoContinueForFollowUp() && this.agent.hasQueuedMessages()) return;
|
|
1326
|
+
const records = this.#pendingIrcAsides;
|
|
1327
|
+
this.#pendingIrcAsides = [];
|
|
1328
|
+
this.#wakeForIrc(records);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/** Fire-and-forget wake turn for incoming IRC — idle delivery and stranded-aside resume both
|
|
1332
|
+
* route here. Wrapped in #beginInFlight/#endInFlight so the turn is tracked and its settle
|
|
1333
|
+
* re-drains anything that stranded during it. A user interrupt may have intentionally left a
|
|
1334
|
+
* follow-up queued behind an invalid tail (seam #5); the wake turn's loop would otherwise drain
|
|
1335
|
+
* it, so park the follow-up queue across the wake and restore it after. It stays queued post-wake
|
|
1336
|
+
* because #canAutoContinueForFollowUp suppresses follow-up auto-resume while a user interrupt is
|
|
1337
|
+
* in effect, even though the wake left a provider-valid tail. */
|
|
1338
|
+
#wakeForIrc(records: CustomMessage[]): void {
|
|
1339
|
+
// Park only a *blocked* follow-up (one a user interrupt is intentionally holding); an
|
|
1340
|
+
// already-resumable follow-up can ride the wake turn normally without reordering.
|
|
1341
|
+
const parkedFollowUps =
|
|
1342
|
+
this.agent.peekSteeringQueue().length === 0 &&
|
|
1343
|
+
this.agent.peekFollowUpQueue().length > 0 &&
|
|
1344
|
+
!this.#canAutoContinueForFollowUp()
|
|
1345
|
+
? [...this.agent.peekFollowUpQueue()]
|
|
1346
|
+
: [];
|
|
1347
|
+
if (parkedFollowUps.length > 0) {
|
|
1348
|
+
this.agent.replaceQueues([...this.agent.peekSteeringQueue()], []);
|
|
1349
|
+
}
|
|
1350
|
+
this.#beginInFlight();
|
|
1351
|
+
void this.agent
|
|
1352
|
+
.prompt(records)
|
|
1353
|
+
.catch(error => {
|
|
1354
|
+
logger.warn("IRC wake turn failed", { error: String(error) });
|
|
1355
|
+
})
|
|
1356
|
+
.finally(() => {
|
|
1357
|
+
if (parkedFollowUps.length > 0) {
|
|
1358
|
+
this.agent.replaceQueues(
|
|
1359
|
+
[...this.agent.peekSteeringQueue()],
|
|
1360
|
+
[...parkedFollowUps, ...this.agent.peekFollowUpQueue()],
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
this.#endInFlight();
|
|
1364
|
+
});
|
|
1264
1365
|
}
|
|
1265
1366
|
|
|
1266
1367
|
/** Remove advisor concern/blocker cards from the agent-core steer/follow-up
|
|
@@ -1281,14 +1382,14 @@ export class AgentSession {
|
|
|
1281
1382
|
}
|
|
1282
1383
|
|
|
1283
1384
|
/** Record a suppressed advisor concern as visible, persisted advice without
|
|
1284
|
-
* triggering a turn. When the agent is idle (the normal post-interrupt case
|
|
1285
|
-
*
|
|
1286
|
-
*
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1289
|
-
* live streamMessage. */
|
|
1385
|
+
* triggering a turn. When the agent is idle (the normal post-interrupt case,
|
|
1386
|
+
* including the post-prompt unwind window where the core loop has ended), emit
|
|
1387
|
+
* message_start/message_end like #flushPendingIrcAsides so #handleAgentEvent
|
|
1388
|
+
* renders it live (TUI/ACP) and persists it as a CustomMessageEntry. Only while
|
|
1389
|
+
* an abort is still tearing a live turn down do we park it hidden, so abort's
|
|
1390
|
+
* settle step replays it once idle — never appended into a live streamMessage. */
|
|
1290
1391
|
#preserveAdvisorCard(card: CustomMessage): void {
|
|
1291
|
-
if (this.isStreaming) {
|
|
1392
|
+
if (this.#abortInProgress && this.isStreaming) {
|
|
1292
1393
|
this.#pendingNextTurnMessages.push(card);
|
|
1293
1394
|
return;
|
|
1294
1395
|
}
|
|
@@ -1528,33 +1629,48 @@ export class AgentSession {
|
|
|
1528
1629
|
// channel (aborting in-flight tools at the next steering boundary); when the
|
|
1529
1630
|
// loop has already yielded, triggerTurn resumes it so the advice is acted on
|
|
1530
1631
|
// immediately rather than waiting for the next user prompt. After a deliberate
|
|
1531
|
-
// user interrupt
|
|
1532
|
-
//
|
|
1533
|
-
//
|
|
1632
|
+
// user interrupt the auto-resume is suppressed — but only while the agent is
|
|
1633
|
+
// idle or still tearing the interrupted turn down: a concern is then recorded
|
|
1634
|
+
// as a visible card and re-enters context when the user resumes. Once a turn
|
|
1635
|
+
// is streaming again (a resume the user already drove) it is steered in live,
|
|
1636
|
+
// since steering an active run auto-resumes nothing; parking it there would
|
|
1637
|
+
// strand the advice and dump the backlog as one burst at the next prompt. A
|
|
1638
|
+
// plain nit always rides the non-interrupting YieldQueue aside.
|
|
1534
1639
|
const enqueueAdvice = (note: string, severity?: AdvisorSeverity) => {
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
});
|
|
1549
|
-
return;
|
|
1550
|
-
}
|
|
1551
|
-
void this.sendCustomMessage(
|
|
1552
|
-
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
1553
|
-
{ deliverAs: "steer", triggerTurn: true },
|
|
1554
|
-
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
1640
|
+
const channel = resolveAdvisorDeliveryChannel({
|
|
1641
|
+
severity,
|
|
1642
|
+
autoResumeSuppressed: this.#advisorAutoResumeSuppressed,
|
|
1643
|
+
// Key on the live agent-core loop, not session `isStreaming` (which also
|
|
1644
|
+
// counts `#promptInFlightCount` during post-turn unwind). Only a running
|
|
1645
|
+
// loop will consume a steer at its next boundary; steering into the unwind
|
|
1646
|
+
// window would strand the card and let #drainStrandedQueuedMessages
|
|
1647
|
+
// auto-resume it despite the user's interrupt.
|
|
1648
|
+
streaming: this.agent.state.isStreaming,
|
|
1649
|
+
aborting: this.#abortInProgress,
|
|
1650
|
+
});
|
|
1651
|
+
if (channel === "aside") {
|
|
1652
|
+
this.yieldQueue.enqueue("advisor", { note, severity });
|
|
1555
1653
|
return;
|
|
1556
1654
|
}
|
|
1557
|
-
|
|
1655
|
+
const notes: AdvisorNote[] = [{ note, severity }];
|
|
1656
|
+
const content = formatAdvisorBatchContent(notes);
|
|
1657
|
+
const details = { notes } satisfies AdvisorMessageDetails;
|
|
1658
|
+
if (channel === "preserve") {
|
|
1659
|
+
this.#preserveAdvisorCard({
|
|
1660
|
+
role: "custom",
|
|
1661
|
+
customType: "advisor",
|
|
1662
|
+
content,
|
|
1663
|
+
display: true,
|
|
1664
|
+
attribution: "agent",
|
|
1665
|
+
details,
|
|
1666
|
+
timestamp: Date.now(),
|
|
1667
|
+
});
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
void this.sendCustomMessage(
|
|
1671
|
+
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
1672
|
+
{ deliverAs: "steer", triggerTurn: true },
|
|
1673
|
+
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
1558
1674
|
};
|
|
1559
1675
|
|
|
1560
1676
|
const adviseTool = new AdviseTool(enqueueAdvice);
|
|
@@ -3644,7 +3760,7 @@ export class AgentSession {
|
|
|
3644
3760
|
*/
|
|
3645
3761
|
beginDispose(): void {
|
|
3646
3762
|
this.#isDisposed = true;
|
|
3647
|
-
this.#
|
|
3763
|
+
this.#flushPendingIrcAsides();
|
|
3648
3764
|
this.yieldQueue.clear();
|
|
3649
3765
|
this.agent.setAsideMessageProvider(undefined);
|
|
3650
3766
|
this.#stopAdvisorRuntime();
|
|
@@ -4495,7 +4611,7 @@ export class AgentSession {
|
|
|
4495
4611
|
});
|
|
4496
4612
|
|
|
4497
4613
|
for (const customTool of mcpTools) {
|
|
4498
|
-
const wrapped = CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool;
|
|
4614
|
+
const wrapped = wrapToolWithMetaNotice(CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool);
|
|
4499
4615
|
const finalTool = (
|
|
4500
4616
|
this.#extensionRunner ? new ExtensionToolWrapper(wrapped, this.#extensionRunner) : wrapped
|
|
4501
4617
|
) as AgentTool;
|
|
@@ -4555,8 +4671,9 @@ export class AgentSession {
|
|
|
4555
4671
|
this.#rpcHostToolNames.clear();
|
|
4556
4672
|
|
|
4557
4673
|
for (const tool of rpcTools) {
|
|
4674
|
+
const metaWrapped = wrapToolWithMetaNotice(tool);
|
|
4558
4675
|
const finalTool = (
|
|
4559
|
-
this.#extensionRunner ? new ExtensionToolWrapper(
|
|
4676
|
+
this.#extensionRunner ? new ExtensionToolWrapper(metaWrapped, this.#extensionRunner) : metaWrapped
|
|
4560
4677
|
) as AgentTool;
|
|
4561
4678
|
this.#toolRegistry.set(finalTool.name, finalTool);
|
|
4562
4679
|
this.#rpcHostToolNames.add(finalTool.name);
|
|
@@ -5122,15 +5239,16 @@ export class AgentSession {
|
|
|
5122
5239
|
if (!options?.streamingBehavior) {
|
|
5123
5240
|
throw new AgentBusyError();
|
|
5124
5241
|
}
|
|
5242
|
+
// Steer/follow-up the keyword notices BEFORE the queued user message so the
|
|
5243
|
+
// model reads the steering notice ahead of the prompt it modifies.
|
|
5244
|
+
for (const notice of keywordNotices) {
|
|
5245
|
+
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
5246
|
+
}
|
|
5125
5247
|
if (options.streamingBehavior === "followUp") {
|
|
5126
5248
|
await this.#queueUserMessage(expandedText, options?.images, "followUp");
|
|
5127
5249
|
} else {
|
|
5128
5250
|
await this.#queueUserMessage(expandedText, options?.images, "steer");
|
|
5129
5251
|
}
|
|
5130
|
-
// Steer/follow-up the keyword notices alongside the queued user message.
|
|
5131
|
-
for (const notice of keywordNotices) {
|
|
5132
|
-
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
5133
|
-
}
|
|
5134
5252
|
return true;
|
|
5135
5253
|
}
|
|
5136
5254
|
|
|
@@ -5169,8 +5287,10 @@ export class AgentSession {
|
|
|
5169
5287
|
await this.#promptWithMessage(message, expandedText, {
|
|
5170
5288
|
...options,
|
|
5171
5289
|
images: normalizedImages,
|
|
5172
|
-
prependMessages:
|
|
5173
|
-
|
|
5290
|
+
prependMessages:
|
|
5291
|
+
preludeMessages.length > 0 || keywordNotices.length > 0
|
|
5292
|
+
? [...preludeMessages, ...keywordNotices]
|
|
5293
|
+
: undefined,
|
|
5174
5294
|
});
|
|
5175
5295
|
} finally {
|
|
5176
5296
|
// Clean up residual eager-todo directive if the prompt never consumed it
|
|
@@ -5209,13 +5329,13 @@ export class AgentSession {
|
|
|
5209
5329
|
if (!options?.streamingBehavior) {
|
|
5210
5330
|
throw new AgentBusyError();
|
|
5211
5331
|
}
|
|
5332
|
+
for (const notice of keywordNotices) {
|
|
5333
|
+
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
5334
|
+
}
|
|
5212
5335
|
await this.sendCustomMessage(message, {
|
|
5213
5336
|
deliverAs: options.streamingBehavior,
|
|
5214
5337
|
queueChipText: options.queueChipText,
|
|
5215
5338
|
});
|
|
5216
|
-
for (const notice of keywordNotices) {
|
|
5217
|
-
await this.sendCustomMessage(notice, { deliverAs: options.streamingBehavior });
|
|
5218
|
-
}
|
|
5219
5339
|
return;
|
|
5220
5340
|
}
|
|
5221
5341
|
|
|
@@ -5231,7 +5351,7 @@ export class AgentSession {
|
|
|
5231
5351
|
|
|
5232
5352
|
await this.#promptWithMessage(customMessage, textContent, {
|
|
5233
5353
|
...options,
|
|
5234
|
-
|
|
5354
|
+
prependMessages: keywordNotices.length > 0 ? keywordNotices : undefined,
|
|
5235
5355
|
});
|
|
5236
5356
|
}
|
|
5237
5357
|
|
|
@@ -5240,7 +5360,6 @@ export class AgentSession {
|
|
|
5240
5360
|
expandedText: string,
|
|
5241
5361
|
options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck"> & {
|
|
5242
5362
|
prependMessages?: AgentMessage[];
|
|
5243
|
-
appendMessages?: AgentMessage[];
|
|
5244
5363
|
skipPostPromptRecoveryWait?: boolean;
|
|
5245
5364
|
},
|
|
5246
5365
|
): Promise<void> {
|
|
@@ -5307,12 +5426,6 @@ export class AgentSession {
|
|
|
5307
5426
|
|
|
5308
5427
|
messages.push(message);
|
|
5309
5428
|
|
|
5310
|
-
// Inject the ultrathink notice (and any other per-turn appends) right after the
|
|
5311
|
-
// user message so the model reads it as part of the same turn.
|
|
5312
|
-
if (options?.appendMessages) {
|
|
5313
|
-
messages.push(...options.appendMessages);
|
|
5314
|
-
}
|
|
5315
|
-
|
|
5316
5429
|
// Early bail-out: if a newer abort/prompt cycle started during setup,
|
|
5317
5430
|
// return before mutating shared state (nextTurn messages, system prompt).
|
|
5318
5431
|
if (this.#promptGeneration !== generation) {
|
|
@@ -5634,12 +5747,24 @@ export class AgentSession {
|
|
|
5634
5747
|
#canAutoContinueForFollowUp(): boolean {
|
|
5635
5748
|
if (this.isStreaming) return false;
|
|
5636
5749
|
if (this.isRetrying) return false;
|
|
5750
|
+
// A queued steer resumes from ANY tail: Agent.continue() runs #runLoop(undefined),
|
|
5751
|
+
// whose initial steering poll injects the steer before the first provider call, so the
|
|
5752
|
+
// request tail becomes the steer (valid) regardless of any injected custom / bashExecution
|
|
5753
|
+
// / pythonExecution record a user interrupt left as the literal transcript tail. This is
|
|
5754
|
+
// why a queued user steer stranded behind a preserved advisor card (or a flushed IRC aside
|
|
5755
|
+
// / eval execution record) still resumes — no tail-role enumeration needed.
|
|
5756
|
+
if (this.agent.peekSteeringQueue().length > 0) return true;
|
|
5757
|
+
// Follow-up-only auto-resume stays suppressed while a deliberate user interrupt is in effect
|
|
5758
|
+
// (#advisorAutoResumeSuppressed, cleared on the next user prompt): the user stopped, so their
|
|
5759
|
+
// queued follow-up waits for an explicit resume — even if an interleaving IRC wake turn has
|
|
5760
|
+
// since left a provider-valid tail.
|
|
5761
|
+
if (this.#advisorAutoResumeSuppressed) return false;
|
|
5762
|
+
// Follow-up-only resume has no steer to inject, so Agent.continue() continues from the
|
|
5763
|
+
// existing context tail — which must itself be a valid provider tail. An injected
|
|
5764
|
+
// non-conversational tail (advisor card → `developer`, bash/python execution) would make
|
|
5765
|
+
// the first model call invalid, so leave the follow-up queued for the next explicit resume.
|
|
5637
5766
|
const messages = this.agent.state.messages;
|
|
5638
5767
|
const last = messages[messages.length - 1];
|
|
5639
|
-
// A user interrupt during tool execution can leave the transcript ending
|
|
5640
|
-
// with the emitted tool result, not the aborted assistant message. Continuing
|
|
5641
|
-
// from that state is still resumable: Agent.continue() first polls queued
|
|
5642
|
-
// steering before making the next model call.
|
|
5643
5768
|
return last?.role === "assistant" || last?.role === "toolResult";
|
|
5644
5769
|
}
|
|
5645
5770
|
|
|
@@ -5872,15 +5997,32 @@ export class AgentSession {
|
|
|
5872
5997
|
});
|
|
5873
5998
|
}
|
|
5874
5999
|
|
|
5875
|
-
/** Clear queued messages and return
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
6000
|
+
/** Clear queued messages and return the user-restorable ones (text plus any attached images).
|
|
6001
|
+
* Only user-authored messages (plain user turns, `attribution:"user"` custom like `/skill`) are
|
|
6002
|
+
* returned for editor restore. Other queued messages stay in the agent-core queues so a continuing
|
|
6003
|
+
* stream still delivers them — EXCEPT on `forInterrupt` (Esc+abort), where only advisor cards are
|
|
6004
|
+
* kept (abort()'s #extractQueuedAdvisorCards preserves them as visible advice) and every other
|
|
6005
|
+
* non-user steer (hidden goal/plan/budget, IRC/extension asides) is dropped, so abort()'s
|
|
6006
|
+
* #drainStrandedQueuedMessages can't auto-resume the run the user just interrupted (the drain only
|
|
6007
|
+
* fires while agent.hasQueuedMessages()). Plain Alt+Up dequeue preserves those non-user steers. */
|
|
6008
|
+
clearQueue(options?: { forInterrupt?: boolean }): {
|
|
6009
|
+
steering: RestoredQueuedMessage[];
|
|
6010
|
+
followUp: RestoredQueuedMessage[];
|
|
6011
|
+
} {
|
|
6012
|
+
const steeringAll = this.agent.peekSteeringQueue();
|
|
6013
|
+
const followUpAll = this.agent.peekFollowUpQueue();
|
|
6014
|
+
const steering = steeringAll.filter(isUserQueuedMessage).map(toRestoredQueuedMessage);
|
|
6015
|
+
const followUp = followUpAll.filter(isUserQueuedMessage).map(toRestoredQueuedMessage);
|
|
6016
|
+
const keep: (m: AgentMessage) => boolean = options?.forInterrupt
|
|
6017
|
+
? isAdvisorCard
|
|
6018
|
+
: m => !isUserQueuedMessage(m) && !isHiddenUserCompanion(m);
|
|
6019
|
+
this.agent.replaceQueues(steeringAll.filter(keep), followUpAll.filter(keep));
|
|
5880
6020
|
return { steering, followUp };
|
|
5881
6021
|
}
|
|
5882
6022
|
|
|
5883
|
-
/** Number of pending displayable messages (includes steering, follow-up, and next-turn messages)
|
|
6023
|
+
/** Number of pending displayable messages (includes steering, follow-up, and next-turn messages).
|
|
6024
|
+
* Reflects actual queued work (advisor cards included) — feeds hasPendingMessages()/RPC and the
|
|
6025
|
+
* empty-submit abort gate. The user-restorable subset is surfaced by getQueuedMessages()/clearQueue(). */
|
|
5884
6026
|
get queuedMessageCount(): number {
|
|
5885
6027
|
return (
|
|
5886
6028
|
this.agent.peekSteeringQueue().filter(isDisplayableQueuedMessage).length +
|
|
@@ -5891,18 +6033,48 @@ export class AgentSession {
|
|
|
5891
6033
|
|
|
5892
6034
|
getQueuedMessages(): { steering: readonly string[]; followUp: readonly string[] } {
|
|
5893
6035
|
return {
|
|
5894
|
-
steering: this.agent.peekSteeringQueue().filter(
|
|
5895
|
-
followUp: this.agent.peekFollowUpQueue().filter(
|
|
6036
|
+
steering: this.agent.peekSteeringQueue().filter(isUserQueuedMessage).map(queueChipText),
|
|
6037
|
+
followUp: this.agent.peekFollowUpQueue().filter(isUserQueuedMessage).map(queueChipText),
|
|
5896
6038
|
};
|
|
5897
6039
|
}
|
|
5898
6040
|
|
|
5899
6041
|
/**
|
|
5900
6042
|
* Pop the last queued message (steering first, then follow-up).
|
|
5901
6043
|
* Used by dequeue keybinding to restore messages to editor one at a time.
|
|
6044
|
+
* Steps over agent-authored queued messages (advisor cards, hidden/internal steers).
|
|
5902
6045
|
*/
|
|
5903
6046
|
popLastQueuedMessage(): RestoredQueuedMessage | undefined {
|
|
5904
|
-
const
|
|
5905
|
-
|
|
6047
|
+
const steering = this.agent.peekSteeringQueue();
|
|
6048
|
+
const followUp = this.agent.peekFollowUpQueue();
|
|
6049
|
+
const lastUserIndex = (queue: readonly AgentMessage[]): number => {
|
|
6050
|
+
for (let i = queue.length - 1; i >= 0; i--) {
|
|
6051
|
+
if (isUserQueuedMessage(queue[i])) return i;
|
|
6052
|
+
}
|
|
6053
|
+
return -1;
|
|
6054
|
+
};
|
|
6055
|
+
// Notices queue immediately before their user message, so dropping the popped
|
|
6056
|
+
// prompt means also dropping the contiguous hidden-user companions right before
|
|
6057
|
+
// it — companions of other queued prompts stay put.
|
|
6058
|
+
const removeWithCompanions = (queue: readonly AgentMessage[], userIndex: number): AgentMessage[] => {
|
|
6059
|
+
let start = userIndex;
|
|
6060
|
+
while (start > 0 && isHiddenUserCompanion(queue[start - 1])) start--;
|
|
6061
|
+
const next = queue.slice();
|
|
6062
|
+
next.splice(start, userIndex - start + 1);
|
|
6063
|
+
return next;
|
|
6064
|
+
};
|
|
6065
|
+
const fromSteer = lastUserIndex(steering);
|
|
6066
|
+
if (fromSteer >= 0) {
|
|
6067
|
+
const removed = steering[fromSteer];
|
|
6068
|
+
this.agent.replaceQueues(removeWithCompanions(steering, fromSteer), followUp.slice());
|
|
6069
|
+
return toRestoredQueuedMessage(removed);
|
|
6070
|
+
}
|
|
6071
|
+
const fromFollowUp = lastUserIndex(followUp);
|
|
6072
|
+
if (fromFollowUp >= 0) {
|
|
6073
|
+
const removed = followUp[fromFollowUp];
|
|
6074
|
+
this.agent.replaceQueues(steering.slice(), removeWithCompanions(followUp, fromFollowUp));
|
|
6075
|
+
return toRestoredQueuedMessage(removed);
|
|
6076
|
+
}
|
|
6077
|
+
return undefined;
|
|
5906
6078
|
}
|
|
5907
6079
|
|
|
5908
6080
|
get skillsSettings(): SkillsSettings | undefined {
|
|
@@ -7992,14 +8164,17 @@ export class AgentSession {
|
|
|
7992
8164
|
maxAttempts: remindersMax,
|
|
7993
8165
|
});
|
|
7994
8166
|
|
|
7995
|
-
|
|
7996
|
-
// Inject reminder and continue the conversation
|
|
7997
|
-
this.agent.appendMessage({
|
|
8167
|
+
const reminderMessage: Message = {
|
|
7998
8168
|
role: "developer",
|
|
7999
8169
|
content: [{ type: "text", text: reminder }],
|
|
8000
8170
|
attribution: "agent",
|
|
8001
8171
|
timestamp: Date.now(),
|
|
8002
|
-
}
|
|
8172
|
+
};
|
|
8173
|
+
|
|
8174
|
+
this.#todoReminderAwaitingProgress = true;
|
|
8175
|
+
// Inject reminder and persist it so the JSONL transcript matches model context.
|
|
8176
|
+
this.agent.appendMessage(reminderMessage);
|
|
8177
|
+
this.sessionManager.appendMessage(reminderMessage);
|
|
8003
8178
|
this.#scheduleAgentContinue({ generation: this.#promptGeneration });
|
|
8004
8179
|
}
|
|
8005
8180
|
|
|
@@ -10192,11 +10367,8 @@ export class AgentSession {
|
|
|
10192
10367
|
if (autoReply) void this.#runIrcAutoReply(msg);
|
|
10193
10368
|
return "injected";
|
|
10194
10369
|
}
|
|
10195
|
-
// Idle:
|
|
10196
|
-
|
|
10197
|
-
this.agent.prompt(record).catch(error => {
|
|
10198
|
-
logger.warn("IRC wake turn failed", { from: msg.from, to: msg.to, error: String(error) });
|
|
10199
|
-
});
|
|
10370
|
+
// Idle: wake a real turn so the recipient responds (shared with the stranded-aside resume).
|
|
10371
|
+
this.#wakeForIrc([record]);
|
|
10200
10372
|
return "woken";
|
|
10201
10373
|
}
|
|
10202
10374
|
|
|
@@ -11381,13 +11553,12 @@ export class AgentSession {
|
|
|
11381
11553
|
}
|
|
11382
11554
|
|
|
11383
11555
|
/**
|
|
11384
|
-
* Format the entire session as plain text for clipboard export
|
|
11385
|
-
*
|
|
11556
|
+
* Format the entire session as plain text for clipboard export: system
|
|
11557
|
+
* prompt, model/thinking config, tool inventory, and the full transcript
|
|
11558
|
+
* rendered with markdown role headings (`## User`, `## Assistant`,
|
|
11559
|
+
* `### Tool Call`/`### Tool Result`).
|
|
11386
11560
|
*/
|
|
11387
|
-
formatSessionAsText(
|
|
11388
|
-
if (options?.compact) {
|
|
11389
|
-
return formatSessionHistoryMarkdown(this.messages);
|
|
11390
|
-
}
|
|
11561
|
+
formatSessionAsText(): string {
|
|
11391
11562
|
return formatSessionDumpText({
|
|
11392
11563
|
messages: this.messages,
|
|
11393
11564
|
systemPrompt: this.agent.state.systemPrompt,
|
package/src/session/artifacts.ts
CHANGED
|
@@ -7,6 +7,24 @@
|
|
|
7
7
|
import * as fs from "node:fs/promises";
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Sanitize a tool name for safe use as the middle segment of the artifact
|
|
12
|
+
* filename (`${id}.${toolType}.log`). Built-in tool names are fixed, but MCP,
|
|
13
|
+
* extension, and RPC-host tool names are arbitrary and may contain path
|
|
14
|
+
* separators (`/`, `\`) or traversal sequences (`..`) that would otherwise let
|
|
15
|
+
* a spilled artifact escape the artifacts directory. Collapse everything
|
|
16
|
+
* outside `[A-Za-z0-9_-]` to `_`, and cap the length so an arbitrarily long
|
|
17
|
+
* name cannot overflow the filesystem's filename limit (ENAMETOOLONG). Fall
|
|
18
|
+
* back to `tool` when nothing survives.
|
|
19
|
+
*/
|
|
20
|
+
function sanitizeToolType(toolType: string): string {
|
|
21
|
+
const sanitized = toolType
|
|
22
|
+
.replace(/[^A-Za-z0-9_-]+/g, "_")
|
|
23
|
+
.slice(0, 64)
|
|
24
|
+
.replace(/^_+|_+$/g, "");
|
|
25
|
+
return sanitized.length > 0 ? sanitized : "tool";
|
|
26
|
+
}
|
|
27
|
+
|
|
10
28
|
/**
|
|
11
29
|
* Manages artifact storage for a session.
|
|
12
30
|
*
|
|
@@ -83,7 +101,7 @@ export class ArtifactManager {
|
|
|
83
101
|
async allocatePath(toolType: string): Promise<{ id: string; path: string }> {
|
|
84
102
|
await this.#ensureDir();
|
|
85
103
|
const id = String(this.allocateId());
|
|
86
|
-
const filename = `${id}.${toolType}.log`;
|
|
104
|
+
const filename = `${id}.${sanitizeToolType(toolType)}.log`;
|
|
87
105
|
return { id, path: path.join(this.#dir, filename) };
|
|
88
106
|
}
|
|
89
107
|
|