@oh-my-pi/pi-coding-agent 16.0.0 → 16.0.1
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 +115 -133
- package/dist/cli.js +158 -130
- package/dist/types/config/settings-schema.d.ts +22 -0
- package/dist/types/discovery/helpers.d.ts +7 -0
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +1 -0
- package/dist/types/extensibility/plugins/runtime-config.d.ts +3 -0
- package/dist/types/modes/types.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +11 -1
- package/dist/types/session/session-manager.d.ts +4 -1
- package/dist/types/task/index.d.ts +21 -0
- package/dist/types/tools/github-cache.d.ts +5 -4
- package/dist/types/tools/job.d.ts +1 -0
- package/dist/types/web/search/index.d.ts +2 -2
- package/dist/types/web/search/provider.d.ts +2 -0
- package/package.json +12 -12
- package/src/cli/args.ts +1 -0
- package/src/collab/host.ts +1 -1
- package/src/config/settings-schema.ts +23 -1
- package/src/discovery/claude-plugins.ts +3 -42
- package/src/discovery/github.ts +101 -6
- package/src/discovery/helpers.ts +11 -0
- package/src/eval/__tests__/prelude-agent.test.ts +73 -0
- package/src/eval/js/shared/prelude.txt +12 -3
- package/src/eval/py/prelude.py +26 -2
- package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
- package/src/extensibility/plugins/loader.ts +3 -2
- package/src/extensibility/plugins/manager.ts +4 -3
- package/src/extensibility/plugins/marketplace/fetcher.ts +32 -34
- package/src/extensibility/plugins/runtime-config.ts +9 -0
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/internal-urls/issue-pr-protocol.ts +8 -4
- package/src/main.ts +5 -1
- package/src/modes/acp/acp-agent.ts +3 -3
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/controllers/extension-ui-controller.ts +4 -3
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/controllers/selector-controller.ts +7 -0
- package/src/modes/interactive-mode.ts +47 -0
- package/src/modes/rpc/rpc-mode.ts +3 -3
- package/src/modes/runtime-init.ts +2 -1
- package/src/modes/types.ts +5 -0
- package/src/prompts/agents/designer.md +8 -0
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/subagent-system-prompt.md +4 -1
- package/src/prompts/tools/eval.md +13 -3
- package/src/prompts/tools/irc.md +1 -1
- package/src/sdk.ts +9 -1
- package/src/session/agent-session.ts +125 -18
- package/src/session/session-manager.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +5 -2
- package/src/task/executor.ts +5 -4
- package/src/task/index.ts +70 -9
- package/src/tools/github-cache.ts +32 -7
- package/src/tools/job.ts +14 -1
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +14 -2
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
type AsideMessage,
|
|
33
33
|
type CompactionSummaryMessage,
|
|
34
34
|
resolveTelemetry,
|
|
35
|
+
STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL,
|
|
35
36
|
ThinkingLevel,
|
|
36
37
|
} from "@oh-my-pi/pi-agent-core";
|
|
37
38
|
|
|
@@ -277,6 +278,7 @@ import {
|
|
|
277
278
|
SILENT_ABORT_MARKER,
|
|
278
279
|
SKILL_PROMPT_MESSAGE_TYPE,
|
|
279
280
|
stripImagesFromMessage,
|
|
281
|
+
USER_INTERRUPT_LABEL,
|
|
280
282
|
} from "./messages";
|
|
281
283
|
import type { SessionContext } from "./session-context";
|
|
282
284
|
import { getLatestCompactionEntry, getRestorableSessionModels } from "./session-context";
|
|
@@ -496,6 +498,12 @@ export interface PromptOptions {
|
|
|
496
498
|
toolChoice?: ToolChoice;
|
|
497
499
|
/** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
|
|
498
500
|
synthetic?: boolean;
|
|
501
|
+
/** Marks this prompt as a deliberate user action (typed message, `.`/`c`
|
|
502
|
+
* continue). Clears advisor auto-resume suppression that a user interrupt set.
|
|
503
|
+
* Defaults to `!synthetic`; manual-continue is synthetic yet user-initiated, so
|
|
504
|
+
* it sets this explicitly. Agent-initiated synthetic prompts (auto-continue,
|
|
505
|
+
* plan re-prime, reminders) leave it unset and keep suppression latched. */
|
|
506
|
+
userInitiated?: boolean;
|
|
499
507
|
/** Explicit billing/initiator attribution for the prompt. Defaults to user prompts as `user` and synthetic prompts as `agent`. */
|
|
500
508
|
attribution?: MessageAttribution;
|
|
501
509
|
/** Skip pre-send compaction checks for this prompt (internal use for maintenance flows). */
|
|
@@ -941,6 +949,10 @@ function isDisplayableQueuedMessage(message: AgentMessage): boolean {
|
|
|
941
949
|
return !(message.role === "custom" && message.display === false);
|
|
942
950
|
}
|
|
943
951
|
|
|
952
|
+
function isAdvisorCard(message: AgentMessage): message is CustomMessage {
|
|
953
|
+
return message.role === "custom" && message.customType === "advisor";
|
|
954
|
+
}
|
|
955
|
+
|
|
944
956
|
function queueChipText(message: AgentMessage): string {
|
|
945
957
|
if (message.role === "custom") {
|
|
946
958
|
return readQueueChipText(message.details) ?? queuedTextContent(message) ?? "";
|
|
@@ -988,10 +1000,15 @@ export class AgentSession {
|
|
|
988
1000
|
#pendingNextTurnMessages: CustomMessage[] = [];
|
|
989
1001
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
990
1002
|
#queuedMessageDrainScheduled = false;
|
|
1003
|
+
/** Latched true when the user deliberately interrupts (USER_INTERRUPT_LABEL);
|
|
1004
|
+
* suppresses advisor concern/blocker auto-resume until the user next resumes.
|
|
1005
|
+
* Advisor advice is still recorded into the transcript, just not auto-run. */
|
|
1006
|
+
#advisorAutoResumeSuppressed = false;
|
|
991
1007
|
#planModeState: PlanModeState | undefined;
|
|
992
1008
|
#goalModeState: GoalModeState | undefined;
|
|
993
1009
|
#goalRuntime: GoalRuntime;
|
|
994
1010
|
#advisorRuntime?: AdvisorRuntime;
|
|
1011
|
+
#advisorEnabled = false;
|
|
995
1012
|
/** The advisor's own agent, retained so `/dump advisor` can serialize its transcript. Undefined when no advisor is active. */
|
|
996
1013
|
#advisorAgent?: Agent;
|
|
997
1014
|
#advisorReadOnlyTools?: AgentTool[];
|
|
@@ -1244,6 +1261,39 @@ export class AgentSession {
|
|
|
1244
1261
|
this.#scheduleQueuedMessageDrain();
|
|
1245
1262
|
}
|
|
1246
1263
|
|
|
1264
|
+
/** Remove advisor concern/blocker cards from the agent-core steer/follow-up
|
|
1265
|
+
* queues and return them. Used on a deliberate user interrupt so the post-abort
|
|
1266
|
+
* stranded-message drain cannot auto-resume the run on an advisor card that was
|
|
1267
|
+
* steered in just before the user stopped; real user follow-ups stay queued.
|
|
1268
|
+
* Synchronous and await-free so it runs before the abort path polls the queue. */
|
|
1269
|
+
#extractQueuedAdvisorCards(): CustomMessage[] {
|
|
1270
|
+
const steering = this.agent.peekSteeringQueue();
|
|
1271
|
+
const followUp = this.agent.peekFollowUpQueue();
|
|
1272
|
+
const cards = [...steering, ...followUp].filter(isAdvisorCard);
|
|
1273
|
+
if (cards.length === 0) return [];
|
|
1274
|
+
this.agent.replaceQueues(
|
|
1275
|
+
steering.filter(m => !isAdvisorCard(m)),
|
|
1276
|
+
followUp.filter(m => !isAdvisorCard(m)),
|
|
1277
|
+
);
|
|
1278
|
+
return cards;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/** Record a suppressed advisor concern as visible, persisted advice without
|
|
1282
|
+
* triggering a turn. When the agent is idle (the normal post-interrupt case),
|
|
1283
|
+
* emit message_start/message_end like #flushPendingIrcAsides so
|
|
1284
|
+
* #handleAgentEvent renders it live (TUI/ACP) and persists it as a
|
|
1285
|
+
* CustomMessageEntry. While a turn is still tearing down (mid-abort), park it
|
|
1286
|
+
* hidden so abort's settle step replays it once idle — never appended into a
|
|
1287
|
+
* live streamMessage. */
|
|
1288
|
+
#preserveAdvisorCard(card: CustomMessage): void {
|
|
1289
|
+
if (this.isStreaming) {
|
|
1290
|
+
this.#pendingNextTurnMessages.push(card);
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
this.agent.emitExternalEvent({ type: "message_start", message: card });
|
|
1294
|
+
this.agent.emitExternalEvent({ type: "message_end", message: card });
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1247
1297
|
#resetInFlight(): void {
|
|
1248
1298
|
this.#promptInFlightCount = 0;
|
|
1249
1299
|
this.#releasePowerAssertion();
|
|
@@ -1443,7 +1493,8 @@ export class AgentSession {
|
|
|
1443
1493
|
},
|
|
1444
1494
|
});
|
|
1445
1495
|
|
|
1446
|
-
|
|
1496
|
+
this.#advisorEnabled = this.settings.get("advisor.enabled") as boolean;
|
|
1497
|
+
if (this.#advisorEnabled) this.#buildAdvisorRuntime();
|
|
1447
1498
|
|
|
1448
1499
|
// Always subscribe to agent events for internal handling
|
|
1449
1500
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
@@ -1457,7 +1508,7 @@ export class AgentSession {
|
|
|
1457
1508
|
#buildAdvisorRuntime(seedToCurrent = false): boolean {
|
|
1458
1509
|
if (this.#isDisposed) return false;
|
|
1459
1510
|
if (this.#advisorRuntime) return true;
|
|
1460
|
-
if (!this
|
|
1511
|
+
if (!this.#advisorEnabled) return false;
|
|
1461
1512
|
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
1462
1513
|
|
|
1463
1514
|
const advisorSel = resolveRoleSelection(
|
|
@@ -1472,23 +1523,33 @@ export class AgentSession {
|
|
|
1472
1523
|
}
|
|
1473
1524
|
|
|
1474
1525
|
// Concern and blocker interrupt the running agent through the steering
|
|
1475
|
-
// channel (aborting in-flight tools at the next steering boundary); when
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1526
|
+
// channel (aborting in-flight tools at the next steering boundary); when the
|
|
1527
|
+
// loop has already yielded, triggerTurn resumes it so the advice is acted on
|
|
1528
|
+
// immediately rather than waiting for the next user prompt. After a deliberate
|
|
1529
|
+
// user interrupt that auto-resume is suppressed: the concern is recorded as
|
|
1530
|
+
// visible advice and re-enters context only when the user resumes. A plain nit
|
|
1531
|
+
// rides the non-interrupting YieldQueue aside.
|
|
1479
1532
|
const enqueueAdvice = (note: string, severity?: AdvisorSeverity) => {
|
|
1480
1533
|
if (isInterruptingSeverity(severity)) {
|
|
1481
1534
|
const notes: AdvisorNote[] = [{ note, severity }];
|
|
1482
|
-
|
|
1483
|
-
|
|
1535
|
+
const content = formatAdvisorBatchContent(notes);
|
|
1536
|
+
const details = { notes } satisfies AdvisorMessageDetails;
|
|
1537
|
+
if (this.#advisorAutoResumeSuppressed) {
|
|
1538
|
+
this.#preserveAdvisorCard({
|
|
1539
|
+
role: "custom",
|
|
1484
1540
|
customType: "advisor",
|
|
1485
|
-
content
|
|
1541
|
+
content,
|
|
1486
1542
|
display: true,
|
|
1487
1543
|
attribution: "agent",
|
|
1488
|
-
details
|
|
1489
|
-
|
|
1544
|
+
details,
|
|
1545
|
+
timestamp: Date.now(),
|
|
1546
|
+
});
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
void this.sendCustomMessage(
|
|
1550
|
+
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
1490
1551
|
{ deliverAs: "steer", triggerTurn: true },
|
|
1491
|
-
).catch(err => logger.debug("advisor
|
|
1552
|
+
).catch(err => logger.debug("advisor delivery failed", { err: String(err) }));
|
|
1492
1553
|
return;
|
|
1493
1554
|
}
|
|
1494
1555
|
this.yieldQueue.enqueue("advisor", { note, severity });
|
|
@@ -5034,6 +5095,13 @@ export class AgentSession {
|
|
|
5034
5095
|
// agent-initiated turns never trigger them.
|
|
5035
5096
|
const keywordNotices = options?.synthetic ? [] : this.#createMagicKeywordNotices(expandedText);
|
|
5036
5097
|
|
|
5098
|
+
// A user-initiated prompt (typed message or the `.`/`c` continue shortcut)
|
|
5099
|
+
// re-enables advisor auto-resume that a prior user interrupt suppressed.
|
|
5100
|
+
// Agent-initiated synthetic prompts (auto-continue, plan, reminders) do not.
|
|
5101
|
+
if (options?.userInitiated ?? !options?.synthetic) {
|
|
5102
|
+
this.#advisorAutoResumeSuppressed = false;
|
|
5103
|
+
}
|
|
5104
|
+
|
|
5037
5105
|
// If streaming, queue via steer() or followUp() based on option
|
|
5038
5106
|
if (this.isStreaming) {
|
|
5039
5107
|
if (!options?.streamingBehavior) {
|
|
@@ -5494,6 +5562,10 @@ export class AgentSession {
|
|
|
5494
5562
|
images: ImageContent[] | undefined,
|
|
5495
5563
|
mode: "steer" | "followUp",
|
|
5496
5564
|
): Promise<void> {
|
|
5565
|
+
// A queued user message (RPC/SDK/collab steer or follow-up, or a typed message
|
|
5566
|
+
// while streaming) is a deliberate resume; re-enable advisor auto-resume that
|
|
5567
|
+
// a user interrupt suppressed.
|
|
5568
|
+
this.#advisorAutoResumeSuppressed = false;
|
|
5497
5569
|
const normalizedImages = await this.#normalizeImagesForModel(images);
|
|
5498
5570
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
5499
5571
|
if (normalizedImages?.length) {
|
|
@@ -5872,6 +5944,12 @@ export class AgentSession {
|
|
|
5872
5944
|
* abort. Omit it for internal/lifecycle aborts.
|
|
5873
5945
|
*/
|
|
5874
5946
|
async abort(options?: { goalReason?: "interrupted" | "internal"; reason?: string }): Promise<void> {
|
|
5947
|
+
const userInterrupt = options?.reason === USER_INTERRUPT_LABEL;
|
|
5948
|
+
if (userInterrupt) this.#advisorAutoResumeSuppressed = true;
|
|
5949
|
+
// Pull advisor concerns out of the steer/follow-up queues before any await so
|
|
5950
|
+
// the post-abort stranded-message drain can't auto-resume the run on them.
|
|
5951
|
+
// They are re-recorded as visible advice once the agent settles (below).
|
|
5952
|
+
const strandedAdvisorCards = userInterrupt ? this.#extractQueuedAdvisorCards() : [];
|
|
5875
5953
|
// Session switch/compact paths disconnect first; explicit aborts should
|
|
5876
5954
|
// leave any queued steer/follow-up visible for the user rather than
|
|
5877
5955
|
// auto-starting a fresh turn during cleanup.
|
|
@@ -5900,6 +5978,19 @@ export class AgentSession {
|
|
|
5900
5978
|
if (this.#toolChoiceQueue.hasInFlight) {
|
|
5901
5979
|
this.#toolChoiceQueue.reject("aborted");
|
|
5902
5980
|
}
|
|
5981
|
+
// Re-record advisor concerns the interrupt would otherwise strand, as
|
|
5982
|
+
// visible/persisted advice without triggering a turn (the agent is idle
|
|
5983
|
+
// now): cards steered into the queue before the user stopped, plus any
|
|
5984
|
+
// that arrived via enqueueAdvice mid-abort and were parked hidden in
|
|
5985
|
+
// #pendingNextTurnMessages while the turn was still tearing down. Other
|
|
5986
|
+
// deferred next-turn context (non-advisor) stays queued, in order.
|
|
5987
|
+
const parkedAdvisorCards = this.#pendingNextTurnMessages.filter(isAdvisorCard);
|
|
5988
|
+
if (parkedAdvisorCards.length > 0) {
|
|
5989
|
+
this.#pendingNextTurnMessages = this.#pendingNextTurnMessages.filter(m => !isAdvisorCard(m));
|
|
5990
|
+
}
|
|
5991
|
+
for (const card of [...strandedAdvisorCards, ...parkedAdvisorCards]) {
|
|
5992
|
+
this.#preserveAdvisorCard(card);
|
|
5993
|
+
}
|
|
5903
5994
|
} finally {
|
|
5904
5995
|
this.#abortInProgress = false;
|
|
5905
5996
|
this.#drainStrandedQueuedMessages();
|
|
@@ -9058,11 +9149,22 @@ export class AgentSession {
|
|
|
9058
9149
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
9059
9150
|
|
|
9060
9151
|
if (this.#isClassifierRefusal(message)) return true;
|
|
9152
|
+
if (this.#streamInterruptedAfterObservableOutput(message)) return false;
|
|
9061
9153
|
if (this.#isStaleOpenAIResponsesReplayError(message)) return true;
|
|
9062
9154
|
|
|
9063
9155
|
const err = message.errorMessage;
|
|
9064
9156
|
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
9065
9157
|
}
|
|
9158
|
+
#streamInterruptedAfterObservableOutput(message: AssistantMessage): boolean {
|
|
9159
|
+
if (message.stopDetails?.type === STREAM_INTERRUPTED_AFTER_CONTENT_STOP_DETAIL) return true;
|
|
9160
|
+
for (const block of message.content) {
|
|
9161
|
+
if (block.type === "toolCall") return true;
|
|
9162
|
+
if (block.type === "text" && block.text.length > 0) return true;
|
|
9163
|
+
if (block.type === "thinking" && block.thinking.length > 0) return true;
|
|
9164
|
+
if (block.type === "redactedThinking" && block.data.length > 0) return true;
|
|
9165
|
+
}
|
|
9166
|
+
return false;
|
|
9167
|
+
}
|
|
9066
9168
|
|
|
9067
9169
|
#isStaleOpenAIResponsesReplayError(message: AssistantMessage): boolean {
|
|
9068
9170
|
const currentApi = this.model?.api;
|
|
@@ -11193,18 +11295,16 @@ export class AgentSession {
|
|
|
11193
11295
|
}
|
|
11194
11296
|
|
|
11195
11297
|
/**
|
|
11196
|
-
* Enable or disable the advisor for this session. The setting is
|
|
11298
|
+
* Enable or disable the advisor for this session. The setting is overridden for the session,
|
|
11197
11299
|
* and the runtime is started or stopped to match.
|
|
11198
11300
|
*
|
|
11199
11301
|
* @returns true when the advisor is actively running after the call.
|
|
11200
11302
|
*/
|
|
11201
11303
|
setAdvisorEnabled(enabled: boolean): boolean {
|
|
11304
|
+
this.#advisorEnabled = enabled;
|
|
11202
11305
|
if (enabled) {
|
|
11203
|
-
this.settings.clearOverride("advisor.enabled");
|
|
11204
|
-
this.settings.set("advisor.enabled", true);
|
|
11205
11306
|
return this.#buildAdvisorRuntime(true);
|
|
11206
11307
|
}
|
|
11207
|
-
this.settings.set("advisor.enabled", false);
|
|
11208
11308
|
this.#stopAdvisorRuntime();
|
|
11209
11309
|
return false;
|
|
11210
11310
|
}
|
|
@@ -11215,7 +11315,14 @@ export class AgentSession {
|
|
|
11215
11315
|
* @returns true when the advisor is actively running after the call.
|
|
11216
11316
|
*/
|
|
11217
11317
|
toggleAdvisorEnabled(): boolean {
|
|
11218
|
-
return this.setAdvisorEnabled(!this
|
|
11318
|
+
return this.setAdvisorEnabled(!this.#advisorEnabled);
|
|
11319
|
+
}
|
|
11320
|
+
|
|
11321
|
+
/**
|
|
11322
|
+
* Whether the advisor setting is enabled for this session.
|
|
11323
|
+
*/
|
|
11324
|
+
isAdvisorEnabled(): boolean {
|
|
11325
|
+
return this.#advisorEnabled;
|
|
11219
11326
|
}
|
|
11220
11327
|
|
|
11221
11328
|
/**
|
|
@@ -11232,7 +11339,7 @@ export class AgentSession {
|
|
|
11232
11339
|
* Return structured advisor stats for the status command and TUI panel.
|
|
11233
11340
|
*/
|
|
11234
11341
|
getAdvisorStats(): AdvisorStats {
|
|
11235
|
-
const configured = this
|
|
11342
|
+
const configured = this.#advisorEnabled;
|
|
11236
11343
|
const advisor = this.#advisorAgent;
|
|
11237
11344
|
if (!advisor) {
|
|
11238
11345
|
return {
|
|
@@ -1522,15 +1522,17 @@ export class SessionManager {
|
|
|
1522
1522
|
/**
|
|
1523
1523
|
* Open a specific session file.
|
|
1524
1524
|
* @param sessionDir Optional dir for /new or /branch; defaults to the file's parent.
|
|
1525
|
+
* @param options.initialCwd Cwd to use when the file is empty or missing.
|
|
1525
1526
|
*/
|
|
1526
1527
|
static async open(
|
|
1527
1528
|
filePath: string,
|
|
1528
1529
|
sessionDir?: string,
|
|
1529
1530
|
storage: SessionStorage = new FileSessionStorage(),
|
|
1531
|
+
options?: { initialCwd?: string },
|
|
1530
1532
|
): Promise<SessionManager> {
|
|
1531
1533
|
const loaded = await loadEntriesFromFile(filePath, storage);
|
|
1532
1534
|
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
1533
|
-
const cwd = header?.cwd ?? getProjectDir();
|
|
1535
|
+
const cwd = header?.cwd ?? options?.initialCwd ?? getProjectDir();
|
|
1534
1536
|
const dir = sessionDir ?? path.dirname(path.resolve(filePath));
|
|
1535
1537
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
1536
1538
|
await manager.setSessionFile(filePath);
|
|
@@ -435,7 +435,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
435
435
|
const { verb, rest } = parseSubcommand(command.args);
|
|
436
436
|
if (!verb || verb === "toggle") {
|
|
437
437
|
const active = runtime.session.toggleAdvisorEnabled();
|
|
438
|
-
const configured = runtime.session.
|
|
438
|
+
const configured = runtime.session.isAdvisorEnabled();
|
|
439
439
|
if (active) {
|
|
440
440
|
await runtime.output("Advisor enabled.");
|
|
441
441
|
} else if (configured) {
|
|
@@ -473,7 +473,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
473
473
|
const { verb, rest } = parseSubcommand(command.args);
|
|
474
474
|
if (!verb || verb === "toggle") {
|
|
475
475
|
const active = runtime.ctx.session.toggleAdvisorEnabled();
|
|
476
|
-
const configured = runtime.ctx.session.
|
|
476
|
+
const configured = runtime.ctx.session.isAdvisorEnabled();
|
|
477
477
|
if (active) {
|
|
478
478
|
runtime.ctx.showStatus("Advisor enabled.");
|
|
479
479
|
} else if (configured) {
|
|
@@ -481,6 +481,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
481
481
|
} else {
|
|
482
482
|
runtime.ctx.showStatus("Advisor disabled.");
|
|
483
483
|
}
|
|
484
|
+
refreshStatusLine(runtime.ctx);
|
|
484
485
|
runtime.ctx.editor.setText("");
|
|
485
486
|
return;
|
|
486
487
|
}
|
|
@@ -489,12 +490,14 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
489
490
|
runtime.ctx.showStatus(
|
|
490
491
|
active ? "Advisor enabled." : "Advisor setting enabled, but no model is assigned to the 'advisor' role.",
|
|
491
492
|
);
|
|
493
|
+
refreshStatusLine(runtime.ctx);
|
|
492
494
|
runtime.ctx.editor.setText("");
|
|
493
495
|
return;
|
|
494
496
|
}
|
|
495
497
|
if (verb === "off") {
|
|
496
498
|
runtime.ctx.session.setAdvisorEnabled(false);
|
|
497
499
|
runtime.ctx.showStatus("Advisor disabled.");
|
|
500
|
+
refreshStatusLine(runtime.ctx);
|
|
498
501
|
runtime.ctx.editor.setText("");
|
|
499
502
|
return;
|
|
500
503
|
}
|
package/src/task/executor.ts
CHANGED
|
@@ -33,7 +33,7 @@ import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage
|
|
|
33
33
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
34
34
|
import type { ArtifactManager } from "../session/artifacts";
|
|
35
35
|
import type { AuthStorage } from "../session/auth-storage";
|
|
36
|
-
import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
|
|
36
|
+
import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
|
|
37
37
|
import { SessionManager } from "../session/session-manager";
|
|
38
38
|
import { truncateTail } from "../session/streaming-output";
|
|
39
39
|
import type { ContextFileEntry } from "../tools";
|
|
@@ -1829,9 +1829,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1829
1829
|
? resolvedThinkingLevel
|
|
1830
1830
|
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
1831
1831
|
|
|
1832
|
+
const effectiveCwd = worktree ?? cwd;
|
|
1832
1833
|
const sessionManager = sessionFile
|
|
1833
|
-
? await awaitAbortable(SessionManager.open(sessionFile))
|
|
1834
|
-
: SessionManager.inMemory(
|
|
1834
|
+
? await awaitAbortable(SessionManager.open(sessionFile, undefined, undefined, { initialCwd: effectiveCwd }))
|
|
1835
|
+
: SessionManager.inMemory(effectiveCwd);
|
|
1835
1836
|
if (options.parentArtifactManager) {
|
|
1836
1837
|
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
1837
1838
|
}
|
|
@@ -2047,7 +2048,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2047
2048
|
{
|
|
2048
2049
|
getModel: () => session.model,
|
|
2049
2050
|
isIdle: () => !session.isStreaming,
|
|
2050
|
-
abort: () => session.abort(),
|
|
2051
|
+
abort: () => session.abort({ reason: USER_INTERRUPT_LABEL }),
|
|
2051
2052
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
2052
2053
|
shutdown: () => {},
|
|
2053
2054
|
getContextUsage: () => session.getContextUsage(),
|
package/src/task/index.ts
CHANGED
|
@@ -366,6 +366,49 @@ export function buildSpecializationAdvisory(
|
|
|
366
366
|
);
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Suggestion — never a rejection — nudging the spawner to coordinate via `irc`
|
|
371
|
+
* when one call creates ≥2 live siblings and it still holds spawn capacity.
|
|
372
|
+
* Returns undefined when there is nothing to coordinate or IRC is unavailable.
|
|
373
|
+
*/
|
|
374
|
+
export function buildCoordinationAdvisory(
|
|
375
|
+
items: TaskItem[],
|
|
376
|
+
depthCapacity: boolean,
|
|
377
|
+
ircEnabled: boolean,
|
|
378
|
+
): string | undefined {
|
|
379
|
+
if (!depthCapacity || !ircEnabled || items.length < 2) return undefined;
|
|
380
|
+
return (
|
|
381
|
+
`Coordinate: ${items.length} siblings are running together. If their work overlaps, have them ` +
|
|
382
|
+
`message each other via \`irc\` (by id, or "all" to broadcast) before editing shared files — ` +
|
|
383
|
+
`live coordination beats a serial handoff. Check \`irc\` op:"list" to see who is doing what.`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Compose the non-blocking advisory appended to a `task` result: the
|
|
389
|
+
* specialization nudge, plus — only when the siblings keep running after this
|
|
390
|
+
* call (`willRunAsync`) — the coordination suggestion. Coordination is gated on
|
|
391
|
+
* async because a sync fanout's siblings have already finished, so a
|
|
392
|
+
* "coordinate while they run" hint would misfire. Returns undefined when
|
|
393
|
+
* neither applies.
|
|
394
|
+
*/
|
|
395
|
+
export function composeSpawnAdvisory(args: {
|
|
396
|
+
agentName: string | undefined;
|
|
397
|
+
items: TaskItem[];
|
|
398
|
+
depthCapacity: boolean;
|
|
399
|
+
ircEnabled: boolean;
|
|
400
|
+
willRunAsync: boolean;
|
|
401
|
+
}): string | undefined {
|
|
402
|
+
return (
|
|
403
|
+
[
|
|
404
|
+
buildSpecializationAdvisory(args.agentName, args.items, args.depthCapacity),
|
|
405
|
+
args.willRunAsync ? buildCoordinationAdvisory(args.items, args.depthCapacity, args.ircEnabled) : undefined,
|
|
406
|
+
]
|
|
407
|
+
.filter(Boolean)
|
|
408
|
+
.join("\n\n") || undefined
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
369
412
|
/** Sentinel for async jobs whose subagent finished with a failing result; progress is already updated. */
|
|
370
413
|
class TaskJobError extends Error {}
|
|
371
414
|
|
|
@@ -539,16 +582,35 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
539
582
|
this.session.settings.get("task.maxRecursionDepth") ?? 2,
|
|
540
583
|
this.session.taskDepth ?? 0,
|
|
541
584
|
);
|
|
542
|
-
const
|
|
585
|
+
const ircEnabled = isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0);
|
|
586
|
+
// Coordination only makes sense when the siblings keep running after this
|
|
587
|
+
// call returns (async). In the sync fallback they have already completed,
|
|
588
|
+
// so a "coordinate while they run" hint would misfire.
|
|
589
|
+
const willRunAsync = !!manager && selectedAgent?.blocking !== true;
|
|
590
|
+
const advisory = this.session.suppressSpawnAdvisory
|
|
591
|
+
? undefined
|
|
592
|
+
: composeSpawnAdvisory({
|
|
593
|
+
agentName: params.agent,
|
|
594
|
+
items: spawnItems,
|
|
595
|
+
depthCapacity,
|
|
596
|
+
ircEnabled,
|
|
597
|
+
willRunAsync,
|
|
598
|
+
});
|
|
599
|
+
// Returns a fresh result (copied content array, copied text part) rather
|
|
600
|
+
// than mutating the caller's — task results are short-lived here, but an
|
|
601
|
+
// in-place edit on a shared/cached AgentToolResult would be a hidden trap.
|
|
543
602
|
const withAdvisory = (result: AgentToolResult<TaskToolDetails>): AgentToolResult<TaskToolDetails> => {
|
|
544
603
|
if (!advisory) return result;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
604
|
+
let appended = false;
|
|
605
|
+
const content = result.content.map(part => {
|
|
606
|
+
if (!appended && part.type === "text" && typeof part.text === "string") {
|
|
607
|
+
appended = true;
|
|
608
|
+
return { ...part, text: `${part.text}\n\n${advisory}` };
|
|
609
|
+
}
|
|
610
|
+
return part;
|
|
611
|
+
});
|
|
612
|
+
if (!appended) content.push({ type: "text", text: advisory });
|
|
613
|
+
return { ...result, content };
|
|
552
614
|
};
|
|
553
615
|
if (!asyncEnabled || !manager || selectedAgent?.blocking === true) {
|
|
554
616
|
// Sync fallback: async execution disabled, orphaned host that never
|
|
@@ -614,7 +676,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
614
676
|
},
|
|
615
677
|
});
|
|
616
678
|
|
|
617
|
-
const ircEnabled = isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0);
|
|
618
679
|
const started: Array<{ agentId: string; jobId: string; description?: string }> = [];
|
|
619
680
|
const failedSchedules: string[] = [];
|
|
620
681
|
for (const spawn of spawns) {
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
* helpers swallow open/IO failures and degrade to "no cache" so a corrupt or
|
|
9
9
|
* unreadable DB never blocks a `gh` call.
|
|
10
10
|
*
|
|
11
|
-
* TTL:
|
|
12
11
|
* Soft TTL → return cached row directly.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Stateful issue/PR rows past soft TTL but within hard TTL → refresh
|
|
13
|
+
* synchronously, falling back to the cached row if the live fetch fails.
|
|
14
|
+
* Expensive PR diff rows past soft TTL but within hard TTL → return cached
|
|
15
|
+
* row AND schedule a background refresh (errors logged, never thrown).
|
|
15
16
|
* Past hard TTL → treat as miss and fetch fresh.
|
|
16
17
|
*/
|
|
17
18
|
|
|
@@ -21,6 +22,7 @@ import * as os from "node:os";
|
|
|
21
22
|
import * as path from "node:path";
|
|
22
23
|
import { getGithubCacheDbPath, logger } from "@oh-my-pi/pi-utils";
|
|
23
24
|
import type { Settings } from "../config/settings";
|
|
25
|
+
import { ToolAbortError } from "./tool-errors";
|
|
24
26
|
|
|
25
27
|
// ────────────────────────────────────────────────────────────────────────────
|
|
26
28
|
// Storage layer
|
|
@@ -449,7 +451,7 @@ export interface CacheLookupOptions<T> {
|
|
|
449
451
|
now?: number;
|
|
450
452
|
}
|
|
451
453
|
|
|
452
|
-
export type CacheStatus = "miss" | "fresh" | "stale" | "disabled";
|
|
454
|
+
export type CacheStatus = "miss" | "fresh" | "refreshed" | "stale" | "disabled";
|
|
453
455
|
|
|
454
456
|
export interface CacheLookupResult<T> {
|
|
455
457
|
rendered: string;
|
|
@@ -595,7 +597,7 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
|
|
|
595
597
|
status: "fresh",
|
|
596
598
|
fetchedAt: cached.fetchedAt,
|
|
597
599
|
};
|
|
598
|
-
} else {
|
|
600
|
+
} else if (options.kind === "pr-diff") {
|
|
599
601
|
scheduleBackgroundRefresh(
|
|
600
602
|
authKey,
|
|
601
603
|
options.repo,
|
|
@@ -611,6 +613,28 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
|
|
|
611
613
|
status: "stale",
|
|
612
614
|
fetchedAt: cached.fetchedAt,
|
|
613
615
|
};
|
|
616
|
+
} else {
|
|
617
|
+
try {
|
|
618
|
+
const fresh = await options.fetchFresh();
|
|
619
|
+
const fetchedAt = Date.now();
|
|
620
|
+
storeResult(authKey, options.repo, options.kind, options.number, options.includeComments, fresh, fetchedAt);
|
|
621
|
+
return { ...fresh, status: "refreshed", fetchedAt };
|
|
622
|
+
} catch (err) {
|
|
623
|
+
if (err instanceof ToolAbortError) throw err;
|
|
624
|
+
logger.debug("github cache: synchronous refresh failed; returning stale view", {
|
|
625
|
+
err: String(err),
|
|
626
|
+
repo: options.repo,
|
|
627
|
+
kind: options.kind,
|
|
628
|
+
number: options.number,
|
|
629
|
+
});
|
|
630
|
+
return {
|
|
631
|
+
rendered: cached.rendered,
|
|
632
|
+
sourceUrl: cached.sourceUrl,
|
|
633
|
+
payload: cached.payload,
|
|
634
|
+
status: "stale",
|
|
635
|
+
fetchedAt: cached.fetchedAt,
|
|
636
|
+
};
|
|
637
|
+
}
|
|
614
638
|
}
|
|
615
639
|
}
|
|
616
640
|
|
|
@@ -624,7 +648,7 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
|
|
|
624
648
|
* Human-friendly freshness note for protocol-handler `notes[]` rendering.
|
|
625
649
|
*/
|
|
626
650
|
export function formatFreshnessNote(status: CacheStatus, fetchedAtMs: number, now: number = Date.now()): string {
|
|
627
|
-
if (status === "miss") return "Fetched live";
|
|
651
|
+
if (status === "miss" || status === "refreshed") return "Fetched live";
|
|
628
652
|
if (status === "disabled") return "Cache disabled; fetched live";
|
|
629
653
|
const ageSec = Math.max(0, Math.round((now - fetchedAtMs) / 1000));
|
|
630
654
|
const human =
|
|
@@ -633,6 +657,7 @@ export function formatFreshnessNote(status: CacheStatus, fetchedAtMs: number, no
|
|
|
633
657
|
: ageSec < 3600
|
|
634
658
|
? `${Math.round(ageSec / 60)}m ago`
|
|
635
659
|
: `${Math.round(ageSec / 3600)}h ago`;
|
|
636
|
-
if (status === "stale")
|
|
660
|
+
if (status === "stale")
|
|
661
|
+
return `WARNING: showing cached content from ${human}; live refresh failed or is still running`;
|
|
637
662
|
return `Cached: ${human}`;
|
|
638
663
|
}
|
package/src/tools/job.ts
CHANGED
|
@@ -372,6 +372,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
372
372
|
interface JobRenderArgs {
|
|
373
373
|
poll?: string[];
|
|
374
374
|
cancel?: string[];
|
|
375
|
+
list?: boolean;
|
|
375
376
|
}
|
|
376
377
|
|
|
377
378
|
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
|
@@ -433,6 +434,7 @@ function flattenStructuredPreview(text: string): string {
|
|
|
433
434
|
}
|
|
434
435
|
|
|
435
436
|
function describeTarget(args: JobRenderArgs | undefined): string {
|
|
437
|
+
if (args?.list) return "background jobs";
|
|
436
438
|
const poll = args?.poll ?? [];
|
|
437
439
|
const cancel = args?.cancel ?? [];
|
|
438
440
|
const parts: string[] = [];
|
|
@@ -460,7 +462,7 @@ export const jobToolRenderer = {
|
|
|
460
462
|
uiTheme: Theme,
|
|
461
463
|
args?: JobRenderArgs,
|
|
462
464
|
): Component {
|
|
463
|
-
|
|
465
|
+
let jobs = result.details?.jobs ?? [];
|
|
464
466
|
|
|
465
467
|
if (jobs.length === 0) {
|
|
466
468
|
const fallback = result.content?.find(c => c.type === "text")?.text || "No jobs to process";
|
|
@@ -468,6 +470,17 @@ export const jobToolRenderer = {
|
|
|
468
470
|
return new Text([header, formatEmptyMessage(fallback, uiTheme)].join("\n"), 0, 0);
|
|
469
471
|
}
|
|
470
472
|
|
|
473
|
+
const isPollCall = args
|
|
474
|
+
? !args.list && (!args.cancel || args.cancel.length === 0 || args.poll !== undefined)
|
|
475
|
+
: true;
|
|
476
|
+
|
|
477
|
+
if (!options.isPartial && isPollCall) {
|
|
478
|
+
jobs = jobs.filter(job => job.status !== "running");
|
|
479
|
+
if (jobs.length === 0) {
|
|
480
|
+
return new Text("", 0, 0);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
471
484
|
const counts = { completed: 0, failed: 0, cancelled: 0, running: 0 };
|
|
472
485
|
for (const job of jobs) counts[job.status]++;
|
|
473
486
|
|
package/src/web/search/index.ts
CHANGED
|
@@ -300,6 +300,6 @@ export function getSearchTools(): CustomTool<any, any>[] {
|
|
|
300
300
|
return [webSearchCustomTool];
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
export { getSearchProvider, setPreferredSearchProvider } from "./provider";
|
|
303
|
+
export { getSearchProvider, setExcludedSearchProviders, setPreferredSearchProvider } from "./provider";
|
|
304
304
|
export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
|
|
305
|
-
export { isSearchProviderPreference } from "./types";
|
|
305
|
+
export { isSearchProviderId, isSearchProviderPreference } from "./types";
|
|
@@ -127,6 +127,18 @@ export function setPreferredSearchProvider(provider: SearchProviderId | "auto"):
|
|
|
127
127
|
preferredProvId = provider;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** Providers excluded from web search resolution via settings. */
|
|
131
|
+
let excludedProvIds = new Set<SearchProviderId>();
|
|
132
|
+
|
|
133
|
+
/** Set providers that web search should never use, including fallbacks. */
|
|
134
|
+
export function setExcludedSearchProviders(providers: readonly SearchProviderId[]): void {
|
|
135
|
+
excludedProvIds = new Set(providers);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isSearchProviderExcluded(id: SearchProviderId): boolean {
|
|
139
|
+
return excludedProvIds.has(id);
|
|
140
|
+
}
|
|
141
|
+
|
|
130
142
|
/**
|
|
131
143
|
* Determine which providers are configured and currently available.
|
|
132
144
|
* Each candidate is loaded (and its `isAvailable()` called) only as the chain
|
|
@@ -138,7 +150,7 @@ export async function resolveProviderChain(
|
|
|
138
150
|
): Promise<SearchProvider[]> {
|
|
139
151
|
const providers: SearchProvider[] = [];
|
|
140
152
|
|
|
141
|
-
if (preferredProvider !== "auto") {
|
|
153
|
+
if (preferredProvider !== "auto" && !isSearchProviderExcluded(preferredProvider)) {
|
|
142
154
|
const provider = await getSearchProvider(preferredProvider);
|
|
143
155
|
if (await provider.isExplicitlyAvailable(authStorage)) {
|
|
144
156
|
providers.push(provider);
|
|
@@ -146,7 +158,7 @@ export async function resolveProviderChain(
|
|
|
146
158
|
}
|
|
147
159
|
|
|
148
160
|
for (const id of SEARCH_PROVIDER_ORDER) {
|
|
149
|
-
if (id === preferredProvider) continue;
|
|
161
|
+
if (id === preferredProvider || isSearchProviderExcluded(id)) continue;
|
|
150
162
|
const provider = await getSearchProvider(id);
|
|
151
163
|
if (await provider.isAvailable(authStorage)) {
|
|
152
164
|
providers.push(provider);
|