@oh-my-pi/pi-coding-agent 15.11.0 → 15.11.2
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 +57 -2
- package/dist/cli.js +678 -657
- package/dist/types/capability/mcp.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +49 -4
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/custom-commands/types.d.ts +6 -3
- package/dist/types/extensibility/custom-tools/loader.d.ts +2 -1
- package/dist/types/extensibility/custom-tools/types.d.ts +8 -4
- package/dist/types/extensibility/extensions/types.d.ts +2 -2
- package/dist/types/extensibility/hooks/types.d.ts +8 -4
- package/dist/types/irc/bus.d.ts +15 -2
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/mcp/transports/stdio.d.ts +1 -0
- package/dist/types/mcp/types.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +3 -0
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/tool-args-reveal.d.ts +43 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +10 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +30 -0
- package/dist/types/modes/theme/theme.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +17 -3
- package/dist/types/slash-commands/available-commands.d.ts +34 -0
- package/dist/types/task/index.d.ts +3 -3
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/browser/attach.d.ts +4 -4
- package/dist/types/tools/browser/registry.d.ts +1 -0
- package/dist/types/tools/irc.d.ts +3 -2
- package/dist/types/tools/path-utils.d.ts +0 -4
- package/dist/types/tools/render-utils.d.ts +22 -0
- package/package.json +11 -11
- package/src/capability/mcp.ts +1 -0
- package/src/cli/gallery-cli.ts +5 -4
- package/src/config/mcp-schema.json +4 -0
- package/src/config/settings-schema.ts +55 -4
- package/src/edit/renderer.ts +96 -46
- package/src/exec/bash-executor.ts +21 -6
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +6 -1
- package/src/extensibility/custom-commands/loader.ts +3 -1
- package/src/extensibility/custom-commands/types.ts +6 -3
- package/src/extensibility/custom-tools/loader.ts +4 -7
- package/src/extensibility/custom-tools/types.ts +8 -4
- package/src/extensibility/extensions/loader.ts +2 -1
- package/src/extensibility/extensions/types.ts +2 -2
- package/src/extensibility/hooks/loader.ts +3 -1
- package/src/extensibility/hooks/types.ts +8 -4
- package/src/internal-urls/docs-index.generated.ts +8 -8
- package/src/irc/bus.ts +14 -3
- package/src/lsp/defaults.json +6 -0
- package/src/lsp/render.ts +2 -28
- package/src/mcp/manager.ts +3 -0
- package/src/mcp/oauth-discovery.ts +27 -2
- package/src/mcp/oauth-flow.ts +47 -1
- package/src/mcp/transports/stdio.ts +3 -0
- package/src/mcp/types.ts +2 -0
- package/src/memories/index.ts +2 -0
- package/src/modes/acp/acp-agent.ts +4 -67
- package/src/modes/components/assistant-message.ts +15 -0
- package/src/modes/components/btw-panel.ts +5 -1
- package/src/modes/components/mcp-add-wizard.ts +13 -0
- package/src/modes/components/plan-review-overlay.ts +32 -3
- package/src/modes/components/settings-selector.ts +2 -0
- package/src/modes/components/status-line/component.ts +22 -12
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/transcript-container.ts +99 -18
- package/src/modes/components/tree-selector.ts +6 -1
- package/src/modes/controllers/event-controller.ts +28 -4
- package/src/modes/controllers/mcp-command-controller.ts +34 -2
- package/src/modes/controllers/selector-controller.ts +4 -0
- package/src/modes/controllers/streaming-reveal.ts +16 -8
- package/src/modes/controllers/tool-args-reveal.ts +174 -0
- package/src/modes/interactive-mode.ts +41 -2
- package/src/modes/rpc/rpc-client.ts +32 -0
- package/src/modes/rpc/rpc-mode.ts +82 -7
- package/src/modes/rpc/rpc-types.ts +23 -0
- package/src/modes/theme/theme.ts +13 -7
- package/src/modes/utils/ui-helpers.ts +13 -4
- package/src/prompts/memories/consolidation_system.md +4 -0
- package/src/prompts/system/irc-autoreply.md +6 -0
- package/src/prompts/system/irc-incoming.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/irc.md +1 -1
- package/src/prompts/tools/task.md +7 -2
- package/src/session/agent-session.ts +120 -10
- package/src/slash-commands/available-commands.ts +105 -0
- package/src/task/index.ts +15 -10
- package/src/task/render.ts +10 -4
- package/src/tools/bash.ts +5 -1
- package/src/tools/browser/attach.ts +26 -7
- package/src/tools/browser/registry.ts +11 -1
- package/src/tools/irc.ts +16 -4
- package/src/tools/job.ts +7 -3
- package/src/tools/path-utils.ts +22 -15
- package/src/tools/render-utils.ts +56 -0
- package/src/tools/write.ts +65 -47
- package/src/web/search/providers/anthropic.ts +29 -4
|
@@ -191,7 +191,11 @@ export class UiHelpers {
|
|
|
191
191
|
this.ctx.chatContainer.addChild(component);
|
|
192
192
|
break;
|
|
193
193
|
}
|
|
194
|
-
if (
|
|
194
|
+
if (
|
|
195
|
+
message.customType === "irc:incoming" ||
|
|
196
|
+
message.customType === "irc:autoreply" ||
|
|
197
|
+
message.customType === "irc:relay"
|
|
198
|
+
) {
|
|
195
199
|
const details = (
|
|
196
200
|
message as CustomMessage<{
|
|
197
201
|
from?: string;
|
|
@@ -201,13 +205,18 @@ export class UiHelpers {
|
|
|
201
205
|
replyTo?: string;
|
|
202
206
|
}>
|
|
203
207
|
).details;
|
|
204
|
-
const
|
|
208
|
+
const kind =
|
|
209
|
+
message.customType === "irc:incoming"
|
|
210
|
+
? ("incoming" as const)
|
|
211
|
+
: message.customType === "irc:autoreply"
|
|
212
|
+
? ("autoreply" as const)
|
|
213
|
+
: ("relay" as const);
|
|
205
214
|
const card = createIrcMessageCard(
|
|
206
215
|
{
|
|
207
|
-
kind
|
|
216
|
+
kind,
|
|
208
217
|
from: details?.from,
|
|
209
218
|
to: details?.to,
|
|
210
|
-
body: incoming ? details?.message : details?.body,
|
|
219
|
+
body: kind === "incoming" ? details?.message : details?.body,
|
|
211
220
|
replyTo: details?.replyTo,
|
|
212
221
|
timestamp: message.timestamp,
|
|
213
222
|
},
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<irc>
|
|
2
|
+
You received an IRC message from agent `{{from}}`{{#if replyTo}} (replying to {{replyTo}}){{/if}} while you are busy mid-task. This is a side-channel turn: reply briefly and directly using the conversation context already available to you. NEVER call tools. The text you write is delivered back to `{{from}}` as your answer.
|
|
3
|
+
|
|
4
|
+
Message:
|
|
5
|
+
{{message}}
|
|
6
|
+
</irc>
|
|
@@ -3,5 +3,5 @@ Incoming IRC message from agent `{{from}}`{{#if replyTo}} (replying to {{replyTo
|
|
|
3
3
|
|
|
4
4
|
{{message}}
|
|
5
5
|
|
|
6
|
-
If a response is expected, reply with the `irc` tool (`op: "send"`, `to: "{{from}}"`) — you may finish your current step first. Nobody replies on your behalf.
|
|
6
|
+
{{#if autoReplied}}You are mid-task, so a side-channel auto-reply was generated from your context and delivered to `{{from}}` on your behalf (recorded after this message). Follow up with the `irc` tool (`op: "send"`, `to: "{{from}}"`) only if that auto-reply needs correcting.{{else}}If a response is expected, reply with the `irc` tool (`op: "send"`, `to: "{{from}}"`) — you may finish your current step first. Nobody replies on your behalf.{{/if}}
|
|
7
7
|
</irc>
|
|
@@ -6,6 +6,7 @@ Executes bash command in shell session for terminal operations like git, bun, ca
|
|
|
6
6
|
- Quote variable expansions like `"$NAME"` to preserve exact content
|
|
7
7
|
- PTY mode is opt-in: set `pty: true` only when the command needs a real terminal (e.g. `sudo`, `ssh` requiring user input); default is `false`
|
|
8
8
|
- Use `;` only when later commands should run regardless of earlier failures
|
|
9
|
+
- Multiple bash calls in one message run concurrently. NEVER split order-dependent commands across parallel calls — chain them with `&&` in a single call.
|
|
9
10
|
- Internal URIs (`skill://`, `agent://`, etc.) are auto-resolved to filesystem paths
|
|
10
11
|
{{#if asyncEnabled}}
|
|
11
12
|
- Use `async: true` for long-running commands when you don't need immediate output; the call returns a background job ID and the result is delivered automatically as a follow-up.
|
package/src/prompts/tools/irc.md
CHANGED
|
@@ -9,7 +9,7 @@ Sends short text messages to other agents in this process and receives theirs.
|
|
|
9
9
|
- `op: "wait"` — block until a message arrives (optionally only `from` a specific peer); consumes and returns it. A timeout is a clean "no message" result, not an error.
|
|
10
10
|
- `op: "inbox"` — drain pending messages without blocking (`peek: true` to leave them unread).
|
|
11
11
|
- `replyTo` — set it to the id of the message you are answering so the sender can correlate.
|
|
12
|
-
- Nobody answers on a peer's behalf
|
|
12
|
+
- Nobody answers on a peer's behalf — a reply normally arrives only when the recipient sends one — with one exception: `send` with `await: true` to a peer that is mid-turn and cannot reach a step boundary (async execution disabled, e.g. blocked in a synchronous task spawn) gets a side-channel auto-reply generated from that peer's context. For background on what a peer has been doing, `read` `history://<id>` instead of interrogating them.
|
|
13
13
|
</instruction>
|
|
14
14
|
|
|
15
15
|
<when_to_use>
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
{{#if batchEnabled}}Spawns subagents to work in the background — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Spawns ONE subagent per call to work in the background.{{/if}}
|
|
1
|
+
{{#if asyncEnabled}}{{#if batchEnabled}}Spawns subagents to work in the background — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Spawns ONE subagent per call to work in the background.{{/if}}
|
|
2
2
|
|
|
3
3
|
- Spawning is non-blocking: the call returns immediately with the agent id{{#if batchEnabled}}s{{/if}} and job id{{#if batchEnabled}}s{{/if}}; each result is delivered automatically when that agent yields.
|
|
4
4
|
- Parallelism = {{#if batchEnabled}}`tasks[]` items in one call, and/or multiple `task` calls in one assistant message{{else}}multiple `task` calls in one assistant message{{/if}}. Concurrency is bounded at {{MAX_CONCURRENCY}} running subagents per session.
|
|
5
5
|
- If genuinely blocked on a result, wait with `job poll`; otherwise keep working. `job cancel` terminates a task and **cannot carry a message** — only for stalled/abandoned work.
|
|
6
|
+
{{else}}{{#if batchEnabled}}Runs subagents synchronously — one per `tasks[]` item; a single spawn is a one-item batch.{{else}}Runs ONE subagent synchronously per call.{{/if}}
|
|
7
|
+
|
|
8
|
+
- Spawning is blocking: the call returns only after the agent{{#if batchEnabled}}s{{/if}} finish; results arrive inline.
|
|
9
|
+
- Parallelism = {{#if batchEnabled}}`tasks[]` items in one call, and/or multiple `task` calls in one assistant message{{else}}multiple `task` calls in one assistant message{{/if}}. Concurrency is bounded at {{MAX_CONCURRENCY}} running subagents per session.
|
|
10
|
+
{{/if}}
|
|
6
11
|
{{#if ircEnabled}}
|
|
7
|
-
- Coordinate with
|
|
12
|
+
- Coordinate with agents via `irc` using their ids. Agents reach you and their siblings live the same way.
|
|
8
13
|
{{/if}}
|
|
9
14
|
|
|
10
15
|
<lifecycle>
|
|
@@ -171,7 +171,7 @@ import { GoalRuntime } from "../goals/runtime";
|
|
|
171
171
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
172
172
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
173
173
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
174
|
-
import type
|
|
174
|
+
import { IrcBus, type IrcMessage } from "../irc/bus";
|
|
175
175
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
176
176
|
import { getMnemopiSessionState, type MnemopiSessionState, setMnemopiSessionState } from "../mnemopi/state";
|
|
177
177
|
import { containsOrchestrate, ORCHESTRATE_NOTICE } from "../modes/orchestrate";
|
|
@@ -185,6 +185,7 @@ import type { PlanModeState } from "../plan-mode/state";
|
|
|
185
185
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
186
186
|
import eagerTodoPrompt from "../prompts/system/eager-todo.md" with { type: "text" };
|
|
187
187
|
import emptyStopRetryTemplate from "../prompts/system/empty-stop-retry.md" with { type: "text" };
|
|
188
|
+
import ircAutoReplyTemplate from "../prompts/system/irc-autoreply.md" with { type: "text" };
|
|
188
189
|
import ircIncomingTemplate from "../prompts/system/irc-incoming.md" with { type: "text" };
|
|
189
190
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
190
191
|
import planModeReferencePrompt from "../prompts/system/plan-mode-reference.md" with { type: "text" };
|
|
@@ -298,6 +299,7 @@ export type AgentSessionEvent =
|
|
|
298
299
|
|
|
299
300
|
/** Listener function for agent session events */
|
|
300
301
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
302
|
+
export type CommandMetadataChangedListener = () => void | Promise<void>;
|
|
301
303
|
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
|
|
302
304
|
|
|
303
305
|
const EMPTY_STOP_MAX_RETRIES = 3;
|
|
@@ -546,6 +548,7 @@ interface ActiveRetryFallbackState {
|
|
|
546
548
|
originalSelector: string;
|
|
547
549
|
originalThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
548
550
|
lastAppliedFallbackThinkingLevel: ConfiguredThinkingLevel | undefined;
|
|
551
|
+
pinned: boolean;
|
|
549
552
|
}
|
|
550
553
|
|
|
551
554
|
function parseRetryFallbackSelector(selector: string): RetryFallbackSelector | undefined {
|
|
@@ -883,6 +886,7 @@ export class AgentSession {
|
|
|
883
886
|
/** Last (enable, providerId) tuple resolved by `#syncAppendOnlyContext` — used to skip no-op invalidations. */
|
|
884
887
|
#lastAppendOnlyResolution?: { enable: boolean; providerId: string | undefined };
|
|
885
888
|
#eventListeners: AgentSessionEventListener[] = [];
|
|
889
|
+
#commandMetadataChangedListeners: CommandMetadataChangedListener[] = [];
|
|
886
890
|
|
|
887
891
|
/** Tracks pending steering messages for UI display. Removed when delivered.
|
|
888
892
|
* Entry shape: `{ text }` for plain-text steers (user-message dequeue
|
|
@@ -3033,6 +3037,27 @@ export class AgentSession {
|
|
|
3033
3037
|
};
|
|
3034
3038
|
}
|
|
3035
3039
|
|
|
3040
|
+
subscribeCommandMetadataChanged(listener: CommandMetadataChangedListener): () => void {
|
|
3041
|
+
this.#commandMetadataChangedListeners.push(listener);
|
|
3042
|
+
return () => {
|
|
3043
|
+
const index = this.#commandMetadataChangedListeners.indexOf(listener);
|
|
3044
|
+
if (index !== -1) {
|
|
3045
|
+
this.#commandMetadataChangedListeners.splice(index, 1);
|
|
3046
|
+
}
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
|
|
3050
|
+
#notifyCommandMetadataChanged(): void {
|
|
3051
|
+
const listeners = [...this.#commandMetadataChangedListeners];
|
|
3052
|
+
for (const listener of listeners) {
|
|
3053
|
+
try {
|
|
3054
|
+
void listener();
|
|
3055
|
+
} catch (err) {
|
|
3056
|
+
logger.error("Command metadata listener threw", { err });
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3036
3061
|
/**
|
|
3037
3062
|
* Temporarily disconnect from agent events.
|
|
3038
3063
|
* User listeners are preserved and will receive events again after resubscribe().
|
|
@@ -4349,9 +4374,15 @@ export class AgentSession {
|
|
|
4349
4374
|
return [...this.#customCommands, ...this.#mcpPromptCommands];
|
|
4350
4375
|
}
|
|
4351
4376
|
|
|
4377
|
+
/** MCP prompt commands only, for command-list metadata. */
|
|
4378
|
+
get mcpPromptCommands(): ReadonlyArray<LoadedCustomCommand> {
|
|
4379
|
+
return this.#mcpPromptCommands;
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4352
4382
|
/** Update the MCP prompt commands list. Called when server prompts are (re)loaded. */
|
|
4353
4383
|
setMCPPromptCommands(commands: LoadedCustomCommand[]): void {
|
|
4354
4384
|
this.#mcpPromptCommands = commands;
|
|
4385
|
+
this.#notifyCommandMetadataChanged();
|
|
4355
4386
|
}
|
|
4356
4387
|
|
|
4357
4388
|
// =========================================================================
|
|
@@ -4464,12 +4495,16 @@ export class AgentSession {
|
|
|
4464
4495
|
return { ...message, content: normalized } as T;
|
|
4465
4496
|
}
|
|
4466
4497
|
|
|
4498
|
+
#magicKeywordEnabled(keyword: "orchestrate" | "ultrathink" | "workflow"): boolean {
|
|
4499
|
+
return this.settings.get("magicKeywords.enabled") && this.settings.get(`magicKeywords.${keyword}`);
|
|
4500
|
+
}
|
|
4501
|
+
|
|
4467
4502
|
#createMagicKeywordNotices(text: string): CustomMessage[] {
|
|
4468
4503
|
const timestamp = Date.now();
|
|
4469
4504
|
const turnBudget = parseTurnBudget(text);
|
|
4470
4505
|
this.sessionManager.beginTurnBudget(turnBudget?.total ?? null, turnBudget?.hard ?? false);
|
|
4471
4506
|
const keywordNotices: CustomMessage[] = [];
|
|
4472
|
-
if (containsUltrathink(text)) {
|
|
4507
|
+
if (this.#magicKeywordEnabled("ultrathink") && containsUltrathink(text)) {
|
|
4473
4508
|
keywordNotices.push({
|
|
4474
4509
|
role: "custom",
|
|
4475
4510
|
customType: "ultrathink-notice",
|
|
@@ -4479,7 +4514,7 @@ export class AgentSession {
|
|
|
4479
4514
|
timestamp,
|
|
4480
4515
|
});
|
|
4481
4516
|
}
|
|
4482
|
-
if (containsOrchestrate(text)) {
|
|
4517
|
+
if (this.#magicKeywordEnabled("orchestrate") && containsOrchestrate(text)) {
|
|
4483
4518
|
keywordNotices.push({
|
|
4484
4519
|
role: "custom",
|
|
4485
4520
|
customType: "orchestrate-notice",
|
|
@@ -4489,7 +4524,7 @@ export class AgentSession {
|
|
|
4489
4524
|
timestamp,
|
|
4490
4525
|
});
|
|
4491
4526
|
}
|
|
4492
|
-
if (containsWorkflow(text)) {
|
|
4527
|
+
if (this.#magicKeywordEnabled("workflow") && containsWorkflow(text)) {
|
|
4493
4528
|
keywordNotices.push({
|
|
4494
4529
|
role: "custom",
|
|
4495
4530
|
customType: "workflow-notice",
|
|
@@ -5920,7 +5955,7 @@ export class AgentSession {
|
|
|
5920
5955
|
if (!model?.reasoning) return;
|
|
5921
5956
|
|
|
5922
5957
|
let resolved: Effort | undefined;
|
|
5923
|
-
if (containsUltrathink(promptText)) {
|
|
5958
|
+
if (this.#magicKeywordEnabled("ultrathink") && containsUltrathink(promptText)) {
|
|
5924
5959
|
// The user explicitly asked for maximum thinking; bypass the classifier
|
|
5925
5960
|
// and jump straight to the highest auto-supported level for this model.
|
|
5926
5961
|
resolved = clampAutoThinkingEffort(model, Effort.XHigh);
|
|
@@ -8257,10 +8292,18 @@ export class AgentSession {
|
|
|
8257
8292
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
8258
8293
|
if (isContextOverflow(message, contextWindow)) return false;
|
|
8259
8294
|
|
|
8295
|
+
if (this.#isClassifierRefusal(message)) return true;
|
|
8296
|
+
|
|
8260
8297
|
const err = message.errorMessage;
|
|
8261
8298
|
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
8262
8299
|
}
|
|
8263
8300
|
|
|
8301
|
+
#isClassifierRefusal(message: AssistantMessage): boolean {
|
|
8302
|
+
if (message.stopReason !== "error") return false;
|
|
8303
|
+
const stopType = message.stopDetails?.type;
|
|
8304
|
+
return stopType === "refusal" || stopType === "sensitive";
|
|
8305
|
+
}
|
|
8306
|
+
|
|
8264
8307
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
8265
8308
|
return (
|
|
8266
8309
|
this.#isTransientEnvelopeErrorMessage(errorMessage) || this.#isTransientTransportErrorMessage(errorMessage)
|
|
@@ -8404,6 +8447,7 @@ export class AgentSession {
|
|
|
8404
8447
|
role: string,
|
|
8405
8448
|
selector: RetryFallbackSelector,
|
|
8406
8449
|
currentSelector: string,
|
|
8450
|
+
options?: { pinFallback?: boolean },
|
|
8407
8451
|
): Promise<void> {
|
|
8408
8452
|
const candidate = this.#modelRegistry.find(selector.provider, selector.id);
|
|
8409
8453
|
if (!candidate) {
|
|
@@ -8429,9 +8473,11 @@ export class AgentSession {
|
|
|
8429
8473
|
originalSelector: currentSelector,
|
|
8430
8474
|
originalThinkingLevel: currentThinkingLevel,
|
|
8431
8475
|
lastAppliedFallbackThinkingLevel: nextThinkingLevel,
|
|
8476
|
+
pinned: options?.pinFallback === true,
|
|
8432
8477
|
};
|
|
8433
8478
|
} else {
|
|
8434
8479
|
this.#activeRetryFallback.lastAppliedFallbackThinkingLevel = nextThinkingLevel;
|
|
8480
|
+
this.#activeRetryFallback.pinned = this.#activeRetryFallback.pinned || options?.pinFallback === true;
|
|
8435
8481
|
}
|
|
8436
8482
|
await this.#emitSessionEvent({
|
|
8437
8483
|
type: "retry_fallback_applied",
|
|
@@ -8441,7 +8487,7 @@ export class AgentSession {
|
|
|
8441
8487
|
});
|
|
8442
8488
|
}
|
|
8443
8489
|
|
|
8444
|
-
async #tryRetryModelFallback(currentSelector: string): Promise<boolean> {
|
|
8490
|
+
async #tryRetryModelFallback(currentSelector: string, options?: { pinFallback?: boolean }): Promise<boolean> {
|
|
8445
8491
|
const role = this.#activeRetryFallback?.role ?? this.#resolveRetryFallbackRole(currentSelector);
|
|
8446
8492
|
if (!role) return false;
|
|
8447
8493
|
|
|
@@ -8451,7 +8497,7 @@ export class AgentSession {
|
|
|
8451
8497
|
if (!candidate) continue;
|
|
8452
8498
|
const apiKey = await this.#modelRegistry.getApiKey(candidate, this.sessionId);
|
|
8453
8499
|
if (!apiKey) continue;
|
|
8454
|
-
await this.#applyRetryFallbackCandidate(role, selector, currentSelector);
|
|
8500
|
+
await this.#applyRetryFallbackCandidate(role, selector, currentSelector, options);
|
|
8455
8501
|
return true;
|
|
8456
8502
|
}
|
|
8457
8503
|
|
|
@@ -8460,6 +8506,7 @@ export class AgentSession {
|
|
|
8460
8506
|
|
|
8461
8507
|
async #maybeRestoreRetryFallbackPrimary(): Promise<void> {
|
|
8462
8508
|
if (!this.#activeRetryFallback) return;
|
|
8509
|
+
if (this.#activeRetryFallback.pinned) return;
|
|
8463
8510
|
if (this.#getRetryFallbackRevertPolicy() !== "cooldown-expiry") return;
|
|
8464
8511
|
|
|
8465
8512
|
const {
|
|
@@ -8557,6 +8604,7 @@ export class AgentSession {
|
|
|
8557
8604
|
async #handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
|
8558
8605
|
const retrySettings = this.settings.getGroup("retry");
|
|
8559
8606
|
if (!retrySettings.enabled) return false;
|
|
8607
|
+
const classifierRefusal = this.#isClassifierRefusal(message);
|
|
8560
8608
|
|
|
8561
8609
|
const generation = this.#promptGeneration;
|
|
8562
8610
|
this.#retryAttempt++;
|
|
@@ -8630,8 +8678,10 @@ export class AgentSession {
|
|
|
8630
8678
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
8631
8679
|
if (!switchedCredential && currentSelector) {
|
|
8632
8680
|
if (retrySettings.modelFallback) {
|
|
8633
|
-
|
|
8634
|
-
|
|
8681
|
+
if (!classifierRefusal) {
|
|
8682
|
+
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
8683
|
+
}
|
|
8684
|
+
switchedModel = await this.#tryRetryModelFallback(currentSelector, { pinFallback: classifierRefusal });
|
|
8635
8685
|
}
|
|
8636
8686
|
if (switchedModel) {
|
|
8637
8687
|
delayMs = 0;
|
|
@@ -8639,6 +8689,11 @@ export class AgentSession {
|
|
|
8639
8689
|
delayMs = parsedRetryAfterMs;
|
|
8640
8690
|
}
|
|
8641
8691
|
}
|
|
8692
|
+
if (classifierRefusal && !switchedModel) {
|
|
8693
|
+
this.#retryAttempt = 0;
|
|
8694
|
+
this.#resolveRetry();
|
|
8695
|
+
return false;
|
|
8696
|
+
}
|
|
8642
8697
|
|
|
8643
8698
|
// Fail-fast cap: if the provider asks us to wait longer than
|
|
8644
8699
|
// retry.maxDelayMs and we have no fallback credential or model to
|
|
@@ -9097,11 +9152,20 @@ export class AgentSession {
|
|
|
9097
9152
|
* → "woken".
|
|
9098
9153
|
*
|
|
9099
9154
|
* Never blocks on the recipient's turn: the wake turn is fire-and-forget.
|
|
9155
|
+
*
|
|
9156
|
+
* When the sender expects a reply (`send await:true`) and this session is
|
|
9157
|
+
* mid-turn with async execution disabled, the next step boundary may be
|
|
9158
|
+
* gated on the sender's own batch finishing (blocking task spawns), so a
|
|
9159
|
+
* real reply turn can never happen in time. In that case an ephemeral
|
|
9160
|
+
* side-channel auto-reply is generated from the current context (the old
|
|
9161
|
+
* `respondAsBackground` path) and sent back over the bus on this agent's
|
|
9162
|
+
* behalf.
|
|
9100
9163
|
*/
|
|
9101
|
-
async deliverIrcMessage(msg: IrcMessage): Promise<"injected" | "woken"> {
|
|
9164
|
+
async deliverIrcMessage(msg: IrcMessage, opts?: { expectsReply?: boolean }): Promise<"injected" | "woken"> {
|
|
9102
9165
|
if (this.#isDisposed) {
|
|
9103
9166
|
throw new Error("Recipient session is disposed.");
|
|
9104
9167
|
}
|
|
9168
|
+
const autoReply = (opts?.expectsReply ?? false) && this.isStreaming && !this.settings.get("async.enabled");
|
|
9105
9169
|
const record: CustomMessage = {
|
|
9106
9170
|
role: "custom",
|
|
9107
9171
|
customType: "irc:incoming",
|
|
@@ -9109,6 +9173,7 @@ export class AgentSession {
|
|
|
9109
9173
|
from: msg.from,
|
|
9110
9174
|
message: msg.body,
|
|
9111
9175
|
replyTo: msg.replyTo ?? "",
|
|
9176
|
+
autoReplied: autoReply,
|
|
9112
9177
|
}),
|
|
9113
9178
|
display: true,
|
|
9114
9179
|
details: { id: msg.id, from: msg.from, message: msg.body, ...(msg.replyTo ? { replyTo: msg.replyTo } : {}) },
|
|
@@ -9118,6 +9183,7 @@ export class AgentSession {
|
|
|
9118
9183
|
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
9119
9184
|
if (this.isStreaming) {
|
|
9120
9185
|
this.#pendingIrcAsides.push(record);
|
|
9186
|
+
if (autoReply) void this.#runIrcAutoReply(msg);
|
|
9121
9187
|
return "injected";
|
|
9122
9188
|
}
|
|
9123
9189
|
// Idle: same wake primitive the yield queue uses for async-result
|
|
@@ -9128,6 +9194,50 @@ export class AgentSession {
|
|
|
9128
9194
|
return "woken";
|
|
9129
9195
|
}
|
|
9130
9196
|
|
|
9197
|
+
/**
|
|
9198
|
+
* Generate and deliver an ephemeral auto-reply to `msg` on this agent's
|
|
9199
|
+
* behalf: a no-tools side-channel turn over the current history (same
|
|
9200
|
+
* pipeline as `/btw`), recorded into this session as an `irc:autoreply`
|
|
9201
|
+
* aside so the model knows what was said for it, and sent back to the
|
|
9202
|
+
* sender as a regular bus message (`replyTo: msg.id`) so their parked
|
|
9203
|
+
* `wait`/`await:true` resolves. Failures only log — the sender then hits
|
|
9204
|
+
* its normal wait timeout.
|
|
9205
|
+
*/
|
|
9206
|
+
async #runIrcAutoReply(msg: IrcMessage): Promise<void> {
|
|
9207
|
+
try {
|
|
9208
|
+
const { replyText } = await this.runEphemeralTurn({
|
|
9209
|
+
promptText: prompt.render(ircAutoReplyTemplate, {
|
|
9210
|
+
from: msg.from,
|
|
9211
|
+
message: msg.body,
|
|
9212
|
+
replyTo: msg.replyTo ?? "",
|
|
9213
|
+
}),
|
|
9214
|
+
});
|
|
9215
|
+
const body = replyText.trim();
|
|
9216
|
+
if (!body || this.#isDisposed) return;
|
|
9217
|
+
const record: CustomMessage = {
|
|
9218
|
+
role: "custom",
|
|
9219
|
+
customType: "irc:autoreply",
|
|
9220
|
+
content: `[IRC you → \`${msg.from}\` (auto)]\n\n${body}`,
|
|
9221
|
+
display: true,
|
|
9222
|
+
details: { to: msg.from, body, replyTo: msg.id },
|
|
9223
|
+
attribution: "agent",
|
|
9224
|
+
timestamp: Date.now(),
|
|
9225
|
+
};
|
|
9226
|
+
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
|
9227
|
+
// Asides drain at the next step boundary; anything left over is
|
|
9228
|
+
// flushed at the start of the next prompt (#flushPendingIrcAsides).
|
|
9229
|
+
this.#pendingIrcAsides.push(record);
|
|
9230
|
+
// `from` must be the id the sender addressed (msg.to) so their
|
|
9231
|
+
// from-filtered waiter matches.
|
|
9232
|
+
const receipt = await IrcBus.global().send({ from: msg.to, to: msg.from, body, replyTo: msg.id });
|
|
9233
|
+
if (receipt.outcome === "failed") {
|
|
9234
|
+
logger.warn("IRC auto-reply delivery failed", { to: msg.from, error: receipt.error });
|
|
9235
|
+
}
|
|
9236
|
+
} catch (error) {
|
|
9237
|
+
logger.warn("IRC auto-reply turn failed", { from: msg.from, error: String(error) });
|
|
9238
|
+
}
|
|
9239
|
+
}
|
|
9240
|
+
|
|
9131
9241
|
/**
|
|
9132
9242
|
* Emit an IRC relay observation event on this session for UI rendering only.
|
|
9133
9243
|
* Does not persist the record to history. Called by the IrcBus to surface
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { AvailableCommand } from "@agentclientprotocol/sdk";
|
|
2
|
+
import type { SkillsSettings } from "../config/settings";
|
|
3
|
+
import type { LoadedCustomCommand } from "../extensibility/custom-commands";
|
|
4
|
+
import type { ExtensionRunner } from "../extensibility/extensions";
|
|
5
|
+
import { getSkillSlashCommandName, type Skill } from "../extensibility/skills";
|
|
6
|
+
import { type FileSlashCommand, loadSlashCommands } from "../extensibility/slash-commands";
|
|
7
|
+
import { ACP_BUILTIN_RESERVED_NAMES, isAcpBuiltinShadowedName } from "./acp-builtins";
|
|
8
|
+
import { BUILTIN_SLASH_COMMANDS_INTERNAL } from "./builtin-registry";
|
|
9
|
+
|
|
10
|
+
export type AvailableSlashCommandSource = "builtin" | "skill" | "extension" | "custom" | "mcp_prompt" | "file";
|
|
11
|
+
|
|
12
|
+
export interface InternalAvailableSlashCommand {
|
|
13
|
+
name: string;
|
|
14
|
+
aliases?: string[];
|
|
15
|
+
description?: string;
|
|
16
|
+
input?: { hint: string };
|
|
17
|
+
subcommands?: Array<{ name: string; description?: string; usage?: string }>;
|
|
18
|
+
source: AvailableSlashCommandSource;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AvailableCommandsSession {
|
|
22
|
+
readonly extensionRunner?: ExtensionRunner;
|
|
23
|
+
readonly customCommands: ReadonlyArray<LoadedCustomCommand>;
|
|
24
|
+
readonly mcpPromptCommands?: ReadonlyArray<LoadedCustomCommand>;
|
|
25
|
+
readonly skills: ReadonlyArray<Skill>;
|
|
26
|
+
readonly skillsSettings?: SkillsSettings;
|
|
27
|
+
setSlashCommands(slashCommands: FileSlashCommand[]): void;
|
|
28
|
+
sessionManager: { getCwd(): string };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function buildAvailableSlashCommands(
|
|
32
|
+
session: AvailableCommandsSession,
|
|
33
|
+
loadFileCommands: (cwd: string) => Promise<FileSlashCommand[]> = cwd => loadSlashCommands({ cwd }),
|
|
34
|
+
): Promise<InternalAvailableSlashCommand[]> {
|
|
35
|
+
const commands: InternalAvailableSlashCommand[] = [];
|
|
36
|
+
const seenNames = new Set<string>();
|
|
37
|
+
const appendCommand = (command: InternalAvailableSlashCommand): void => {
|
|
38
|
+
if (seenNames.has(command.name)) return;
|
|
39
|
+
seenNames.add(command.name);
|
|
40
|
+
commands.push(command);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (const command of BUILTIN_SLASH_COMMANDS_INTERNAL) {
|
|
44
|
+
if (!command.handle) continue;
|
|
45
|
+
const hint = command.acpInputHint ?? command.inlineHint;
|
|
46
|
+
appendCommand({
|
|
47
|
+
name: command.name,
|
|
48
|
+
aliases: command.aliases,
|
|
49
|
+
description: command.acpDescription ?? command.description,
|
|
50
|
+
input: hint ? { hint } : undefined,
|
|
51
|
+
subcommands: command.subcommands,
|
|
52
|
+
source: "builtin",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (session.skillsSettings?.enableSkillCommands) {
|
|
57
|
+
for (const skill of session.skills) {
|
|
58
|
+
appendCommand({
|
|
59
|
+
name: getSkillSlashCommandName(skill),
|
|
60
|
+
description: skill.description || `Run ${skill.name} skill`,
|
|
61
|
+
input: { hint: "arguments" },
|
|
62
|
+
source: "skill",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const runner = session.extensionRunner;
|
|
68
|
+
if (runner) {
|
|
69
|
+
for (const command of runner.getRegisteredCommands(ACP_BUILTIN_RESERVED_NAMES)) {
|
|
70
|
+
if (isAcpBuiltinShadowedName(command.name)) continue;
|
|
71
|
+
appendCommand({
|
|
72
|
+
name: command.name,
|
|
73
|
+
description: command.description ?? "(extension command)",
|
|
74
|
+
input: { hint: "arguments" },
|
|
75
|
+
source: "extension",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const command of session.customCommands) {
|
|
81
|
+
const source: AvailableSlashCommandSource = command.path?.startsWith("mcp:") ? "mcp_prompt" : "custom";
|
|
82
|
+
appendCommand({
|
|
83
|
+
name: command.command.name,
|
|
84
|
+
description: command.command.description,
|
|
85
|
+
input: { hint: "arguments" },
|
|
86
|
+
source,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const fileCommands = await loadFileCommands(session.sessionManager.getCwd());
|
|
91
|
+
session.setSlashCommands(fileCommands);
|
|
92
|
+
for (const command of fileCommands) {
|
|
93
|
+
appendCommand({ name: command.name, description: command.description, source: "file" });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return commands;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function toAcpAvailableCommands(commands: readonly InternalAvailableSlashCommand[]): AvailableCommand[] {
|
|
100
|
+
return commands.map(command => ({
|
|
101
|
+
name: command.name,
|
|
102
|
+
description: command.description ?? "",
|
|
103
|
+
input: command.input,
|
|
104
|
+
}));
|
|
105
|
+
}
|
package/src/task/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Supports:
|
|
10
10
|
* - Single agent spawn per call (parallelism = parallel task calls)
|
|
11
11
|
* - Batch spawning + shared context per call when `task.batch` is enabled
|
|
12
|
-
* -
|
|
12
|
+
* - Background execution through AsyncJobManager when `async.enabled` is enabled
|
|
13
13
|
* - Progress tracking via JSON events
|
|
14
14
|
* - Session artifacts for debugging
|
|
15
15
|
*/
|
|
@@ -190,6 +190,7 @@ function renderDescription(
|
|
|
190
190
|
isolationEnabled: boolean,
|
|
191
191
|
disabledAgents: string[],
|
|
192
192
|
batchEnabled: boolean,
|
|
193
|
+
asyncEnabled: boolean,
|
|
193
194
|
ircEnabled: boolean,
|
|
194
195
|
parentSpawns: string,
|
|
195
196
|
): string {
|
|
@@ -217,6 +218,7 @@ function renderDescription(
|
|
|
217
218
|
MAX_CONCURRENCY: maxConcurrency,
|
|
218
219
|
isolationEnabled,
|
|
219
220
|
batchEnabled,
|
|
221
|
+
asyncEnabled,
|
|
220
222
|
ircEnabled,
|
|
221
223
|
});
|
|
222
224
|
}
|
|
@@ -374,8 +376,8 @@ function discoverAgentsForCreate(cwd: string): Promise<DiscoveryResult> {
|
|
|
374
376
|
* Task tool - Delegate tasks to specialized agents.
|
|
375
377
|
*
|
|
376
378
|
* Each call spawns one subagent — or, with `task.batch`, one per `tasks[]`
|
|
377
|
-
* item.
|
|
378
|
-
*
|
|
379
|
+
* item. When `async.enabled` is on, spawns run as AsyncJobManager jobs; when
|
|
380
|
+
* disabled, the tool blocks until every spawn finishes.
|
|
379
381
|
*/
|
|
380
382
|
export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetails, Theme> {
|
|
381
383
|
readonly name = "task";
|
|
@@ -411,7 +413,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
411
413
|
return lines;
|
|
412
414
|
};
|
|
413
415
|
readonly label = "Task";
|
|
414
|
-
readonly summary = "Spawn
|
|
416
|
+
readonly summary = "Spawn subagents to complete delegated tasks";
|
|
415
417
|
readonly strict = true;
|
|
416
418
|
readonly loadMode = "discoverable";
|
|
417
419
|
readonly renderResult = renderResult;
|
|
@@ -448,6 +450,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
448
450
|
isolationMode !== "none",
|
|
449
451
|
disabledAgents,
|
|
450
452
|
this.#isBatchEnabled(),
|
|
453
|
+
this.session.settings.get("async.enabled"),
|
|
451
454
|
isIrcEnabled(this.session.settings, this.session.taskDepth ?? 0),
|
|
452
455
|
this.session.getSessionSpawns() ?? "*",
|
|
453
456
|
);
|
|
@@ -492,12 +495,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
492
495
|
|
|
493
496
|
const spawnItems = resolveSpawnItems(params);
|
|
494
497
|
const selectedAgent = this.#discoveredAgents.find(agent => agent.name === params.agent);
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
|
|
498
|
+
const asyncEnabled = this.session.settings.get("async.enabled");
|
|
499
|
+
const manager = asyncEnabled ? this.session.asyncJobManager : undefined;
|
|
500
|
+
if (!asyncEnabled || !manager || selectedAgent?.blocking === true) {
|
|
501
|
+
// Sync fallback: async execution disabled, orphaned host that never
|
|
502
|
+
// wired a job manager, or an agent definition that declares
|
|
503
|
+
// `blocking: true`. The session-scoped semaphore still bounds fan-out
|
|
504
|
+
// across parallel task calls.
|
|
505
|
+
if (asyncEnabled && !manager) {
|
|
501
506
|
logger.warn("task: no AsyncJobManager registered; falling back to sync execution");
|
|
502
507
|
}
|
|
503
508
|
return this.#executeSyncFanout(toolCallId, params, spawnItems, signal, onUpdate);
|
package/src/task/render.ts
CHANGED
|
@@ -631,12 +631,18 @@ export function renderCall(
|
|
|
631
631
|
// same agent (and the assignment brief) itself, so showing it here would
|
|
632
632
|
// repeat what the result frame already shows.
|
|
633
633
|
if (!options.renderContext?.hasResult) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
634
|
+
// Mirror renderResult's layout — context, assignment, then the
|
|
635
|
+
// per-agent list — so the agent rows do not jump from above the
|
|
636
|
+
// brief to below it when the first progress snapshot replaces the
|
|
637
|
+
// call view. This also matches the schema's field order (`context`
|
|
638
|
+
// streams before `tasks`), so the streaming preview grows
|
|
639
|
+
// append-only instead of inserting agent rows above the
|
|
640
|
+
// already-rendered markdown and pushing it down on every item.
|
|
638
641
|
if (contextSection) sections.push(contextSection(width));
|
|
639
642
|
if (assignmentSection) sections.push(assignmentSection(width));
|
|
643
|
+
const callLines = renderTaskCallLines(args, theme);
|
|
644
|
+
// Guarded: an empty trailing section would still draw its divider.
|
|
645
|
+
if (callLines.length > 0) sections.push({ separator: true, lines: callLines });
|
|
640
646
|
}
|
|
641
647
|
|
|
642
648
|
return {
|
package/src/tools/bash.ts
CHANGED
|
@@ -368,7 +368,11 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
368
368
|
readonly loadMode = "essential";
|
|
369
369
|
readonly description: string;
|
|
370
370
|
readonly parameters: BashToolSchema;
|
|
371
|
-
|
|
371
|
+
// Non-pty calls run alongside each other (the executor isolates overlapping
|
|
372
|
+
// runs on the same shell session); pty takes over the terminal UI and must
|
|
373
|
+
// run alone.
|
|
374
|
+
readonly concurrency = (args: Partial<BashToolInput>): "shared" | "exclusive" =>
|
|
375
|
+
args.pty === true ? "exclusive" : "shared";
|
|
372
376
|
readonly strict = true;
|
|
373
377
|
readonly #asyncEnabled: boolean;
|
|
374
378
|
readonly #autoBackgroundEnabled: boolean;
|