@oh-my-pi/pi-coding-agent 16.3.0 → 16.3.3
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 +91 -0
- package/dist/cli.js +3855 -3798
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/cli/update-cli.test.d.ts +1 -0
- package/dist/types/commands/update.d.ts +5 -0
- package/dist/types/config/config-file.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/dap/client.d.ts +9 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/edit/file-snapshot-store.d.ts +9 -1
- package/dist/types/edit/hashline/execute.d.ts +1 -1
- package/dist/types/edit/hashline/params.d.ts +3 -6
- package/dist/types/edit/renderer.d.ts +1 -0
- package/dist/types/extensibility/plugins/parser.d.ts +2 -1
- package/dist/types/hindsight/client.d.ts +15 -11
- package/dist/types/hindsight/client.test.d.ts +1 -0
- package/dist/types/irc/bus.d.ts +5 -4
- package/dist/types/mcp/smithery-registry.d.ts +1 -0
- package/dist/types/mcp/smithery-registry.test.d.ts +1 -0
- package/dist/types/modes/components/advisor-message.d.ts +4 -2
- package/dist/types/modes/components/tool-execution.d.ts +9 -6
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +13 -13
- package/dist/types/session/indexed-session-storage.d.ts +2 -2
- package/dist/types/session/session-storage.d.ts +31 -3
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +19 -0
- package/dist/types/ssh/sshfs-mount.d.ts +10 -1
- package/dist/types/subprocess/worker-client.d.ts +20 -6
- package/dist/types/task/discovery.d.ts +5 -2
- package/dist/types/task/renderer.d.ts +1 -0
- package/dist/types/tools/ast-grep.d.ts +4 -2
- package/dist/types/tools/bash.d.ts +27 -0
- package/dist/types/tools/browser/render.d.ts +2 -0
- package/dist/types/tools/debug.d.ts +1 -0
- package/dist/types/tools/eval-render.d.ts +2 -0
- package/dist/types/tools/glob.d.ts +4 -2
- package/dist/types/tools/grep.d.ts +4 -3
- package/dist/types/tools/path-utils.d.ts +7 -0
- package/dist/types/tools/renderers.d.ts +24 -0
- package/dist/types/tools/ssh.d.ts +4 -1
- package/dist/types/tts/index.d.ts +2 -0
- package/dist/types/tts/speakable.d.ts +47 -0
- package/dist/types/tts/speech-enhancer.d.ts +46 -0
- package/dist/types/tts/streaming-player.d.ts +1 -2
- package/dist/types/tts/tts-client.d.ts +11 -10
- package/dist/types/tts/tts-protocol.d.ts +7 -0
- package/dist/types/tts/vocalizer.d.ts +15 -8
- package/dist/types/utils/fetch-timeout.d.ts +4 -0
- package/dist/types/utils/git.d.ts +2 -0
- package/dist/types/web/search/providers/base.d.ts +1 -0
- package/dist/types/web/search/providers/gemini.d.ts +1 -0
- package/package.json +12 -12
- package/scripts/generate-legacy-pi-bundled-registry.ts +8 -2
- package/src/advisor/__tests__/advisor.test.ts +1 -1
- package/src/cli/gallery-fixtures/fs.ts +2 -2
- package/src/cli/gallery-fixtures/search.ts +2 -2
- package/src/cli/models-cli.ts +19 -0
- package/src/cli/update-cli.test.ts +28 -0
- package/src/cli/update-cli.ts +35 -8
- package/src/cli.ts +27 -5
- package/src/commands/update.ts +8 -2
- package/src/config/config-file.ts +6 -6
- package/src/config/model-resolver.ts +31 -10
- package/src/config/settings-schema.ts +21 -0
- package/src/cursor.ts +1 -1
- package/src/dap/client.ts +134 -36
- package/src/discovery/helpers.ts +8 -0
- package/src/edit/file-snapshot-store.ts +12 -1
- package/src/edit/hashline/diff.ts +4 -13
- package/src/edit/hashline/execute.ts +1 -1
- package/src/edit/hashline/params.ts +5 -12
- package/src/edit/renderer.ts +4 -2
- package/src/edit/streaming.ts +15 -5
- package/src/export/html/tool-views.generated.js +34 -34
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/extensions/loader.ts +10 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +2 -2
- package/src/extensibility/plugins/installer.ts +12 -3
- package/src/extensibility/plugins/legacy-pi-compat.ts +87 -11
- package/src/extensibility/plugins/loader.ts +30 -1
- package/src/extensibility/plugins/manager.ts +32 -8
- package/src/extensibility/plugins/parser.ts +7 -5
- package/src/extensibility/tool-event-input.ts +1 -1
- package/src/hindsight/client.test.ts +33 -0
- package/src/hindsight/client.ts +42 -22
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/irc/bus.ts +5 -4
- package/src/main.ts +7 -1
- package/src/mcp/oauth-flow.ts +93 -4
- package/src/mcp/smithery-auth.ts +3 -0
- package/src/mcp/smithery-connect.ts +9 -0
- package/src/mcp/smithery-registry.test.ts +51 -0
- package/src/mcp/smithery-registry.ts +27 -4
- package/src/modes/components/__tests__/move-overlay.test.ts +72 -1
- package/src/modes/components/advisor-message.ts +13 -10
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/move-overlay.ts +35 -23
- package/src/modes/components/status-line/component.ts +11 -4
- package/src/modes/components/tool-execution.ts +119 -20
- package/src/modes/components/transcript-container.ts +26 -0
- package/src/modes/components/tree-selector.ts +10 -3
- package/src/modes/controllers/event-controller.ts +23 -3
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/rpc/rpc-client.ts +29 -16
- package/src/modes/theme/shimmer.ts +49 -15
- package/src/modes/theme/theme.ts +7 -0
- package/src/prompts/goals/goal-mode-context.md +4 -0
- package/src/prompts/goals/goal-todo-context.md +10 -2
- package/src/prompts/system/speech-rewrite.md +15 -0
- package/src/prompts/system/tiny-title-system.md +1 -1
- package/src/prompts/system/title-system-marker.md +2 -1
- package/src/prompts/system/title-system.md +2 -1
- package/src/prompts/tools/ast-grep.md +3 -3
- package/src/prompts/tools/glob.md +1 -1
- package/src/prompts/tools/grep.md +1 -1
- package/src/sdk.ts +8 -5
- package/src/session/agent-session.ts +329 -79
- package/src/session/indexed-session-storage.ts +40 -3
- package/src/session/session-history-format.ts +6 -2
- package/src/session/session-manager.ts +83 -14
- package/src/session/session-storage.ts +112 -26
- package/src/slash-commands/helpers/usage-report.ts +6 -1
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +61 -0
- package/src/ssh/__tests__/sshfs-mount.test.ts +13 -0
- package/src/ssh/connection-manager.ts +44 -11
- package/src/ssh/sshfs-mount.ts +27 -4
- package/src/subprocess/worker-client.ts +161 -10
- package/src/task/discovery.ts +25 -2
- package/src/task/executor.ts +24 -4
- package/src/task/render.ts +26 -12
- package/src/task/renderer.ts +1 -0
- package/src/task/worktree.ts +144 -16
- package/src/tiny/text.ts +109 -17
- package/src/tools/ast-grep.ts +20 -17
- package/src/tools/bash.ts +46 -9
- package/src/tools/browser/render.ts +2 -0
- package/src/tools/debug.ts +1 -0
- package/src/tools/eval-render.ts +5 -2
- package/src/tools/gh-renderer.ts +3 -0
- package/src/tools/glob.ts +24 -20
- package/src/tools/grep.ts +39 -37
- package/src/tools/path-utils.ts +55 -10
- package/src/tools/read.ts +23 -6
- package/src/tools/renderers.ts +24 -0
- package/src/tools/ssh.ts +25 -7
- package/src/tts/index.ts +2 -0
- package/src/tts/speakable.ts +382 -0
- package/src/tts/speech-enhancer.ts +204 -0
- package/src/tts/streaming-player.ts +71 -16
- package/src/tts/tts-client.ts +11 -10
- package/src/tts/tts-protocol.ts +14 -5
- package/src/tts/tts-worker.ts +52 -49
- package/src/tts/vocalizer.ts +277 -46
- package/src/utils/clipboard.ts +49 -12
- package/src/utils/fetch-timeout.ts +10 -0
- package/src/utils/git.ts +8 -0
- package/src/web/search/index.ts +8 -0
- package/src/web/search/providers/base.ts +1 -0
- package/src/web/search/providers/gemini.ts +23 -6
|
@@ -121,6 +121,7 @@ import { getSupportedEfforts } from "@oh-my-pi/pi-catalog/model-thinking";
|
|
|
121
121
|
import { modelsAreEqual } from "@oh-my-pi/pi-catalog/models";
|
|
122
122
|
import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
|
|
123
123
|
import {
|
|
124
|
+
escapeXmlText,
|
|
124
125
|
extractRetryHint,
|
|
125
126
|
formatDuration,
|
|
126
127
|
getAgentDbPath,
|
|
@@ -237,6 +238,7 @@ import { containsWorkflow, WORKFLOW_NOTICE } from "../modes/workflow";
|
|
|
237
238
|
import { createPlanReadMatcher } from "../plan-mode/plan-protection";
|
|
238
239
|
import type { PlanModeState } from "../plan-mode/state";
|
|
239
240
|
import advisorSystemPrompt from "../prompts/advisor/system.md" with { type: "text" };
|
|
241
|
+
import goalModeContextPrompt from "../prompts/goals/goal-mode-context.md" with { type: "text" };
|
|
240
242
|
import goalTodoContextPrompt from "../prompts/goals/goal-todo-context.md" with { type: "text" };
|
|
241
243
|
import parentIrcSteerTemplate from "../prompts/steering/parent-irc.md" with { type: "text" };
|
|
242
244
|
import autoContinuePrompt from "../prompts/system/auto-continue.md" with { type: "text" };
|
|
@@ -358,6 +360,8 @@ import { classifyUnexpectedStop, isUnexpectedStopCandidate } from "./unexpected-
|
|
|
358
360
|
import { YieldQueue } from "./yield-queue";
|
|
359
361
|
|
|
360
362
|
const SESSION_STOP_CONTINUATION_CAP = 8;
|
|
363
|
+
const PLAN_MODE_REMINDER_MAX = 3;
|
|
364
|
+
const PLAN_DECISION_TOOLS = new Set(["ask", "resolve"]);
|
|
361
365
|
|
|
362
366
|
/**
|
|
363
367
|
* Mutating tool results (`bash`/`eval`/`edit`/`write`/`ast_edit`) without the
|
|
@@ -965,6 +969,18 @@ interface ActiveAdvisor {
|
|
|
965
969
|
agentUnsubscribe?: () => void;
|
|
966
970
|
model: Model;
|
|
967
971
|
thinkingLevel: ThinkingLevel;
|
|
972
|
+
/** Stable key for the resolved runtime inputs that require a rebuild to change. */
|
|
973
|
+
signature: string;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/** Resolved advisor config ready to instantiate as an {@link ActiveAdvisor}. */
|
|
977
|
+
interface AdvisorRuntimeDescriptor {
|
|
978
|
+
config: AdvisorConfig;
|
|
979
|
+
name: string;
|
|
980
|
+
slug: string;
|
|
981
|
+
model: Model;
|
|
982
|
+
thinkingLevel: ThinkingLevel;
|
|
983
|
+
signature: string;
|
|
968
984
|
}
|
|
969
985
|
|
|
970
986
|
export interface FreshSessionResult {
|
|
@@ -1587,6 +1603,8 @@ export class AgentSession {
|
|
|
1587
1603
|
/** Mid-run nudges fired this prompt cycle; capped by
|
|
1588
1604
|
* {@link MID_RUN_TODO_NUDGE_MAX_PER_CYCLE}, reset with the counter above. */
|
|
1589
1605
|
#midRunNudgeCount = 0;
|
|
1606
|
+
#planModeReminderCount = 0;
|
|
1607
|
+
#planModeReminderAwaitingProgress = false;
|
|
1590
1608
|
#todoPhases: TodoPhase[] = [];
|
|
1591
1609
|
#replanTitleRefreshInFlight: Promise<void> | undefined = undefined;
|
|
1592
1610
|
/** Resolved TITLE_SYSTEM.md override applied to every automatic session-title
|
|
@@ -1861,6 +1879,21 @@ export class AgentSession {
|
|
|
1861
1879
|
const records = [...this.#pendingIrcInterrupts, ...this.#pendingIrcAsides];
|
|
1862
1880
|
this.#pendingIrcInterrupts = [];
|
|
1863
1881
|
this.#pendingIrcAsides = [];
|
|
1882
|
+
if (this.#planModeState?.enabled) {
|
|
1883
|
+
// Plan mode: fold stranded IRC asides into context without waking an
|
|
1884
|
+
// autonomous turn. Convergence to ask/resolve stays user-driven.
|
|
1885
|
+
for (const record of records) {
|
|
1886
|
+
this.agent.appendMessage(record);
|
|
1887
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
1888
|
+
record.customType,
|
|
1889
|
+
record.content,
|
|
1890
|
+
record.display,
|
|
1891
|
+
record.details,
|
|
1892
|
+
record.attribution ?? "agent",
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1864
1897
|
this.#wakeForIrc(records);
|
|
1865
1898
|
}
|
|
1866
1899
|
|
|
@@ -2247,32 +2280,10 @@ export class AgentSession {
|
|
|
2247
2280
|
}
|
|
2248
2281
|
}
|
|
2249
2282
|
|
|
2250
|
-
#
|
|
2251
|
-
if (this.#isDisposed) return false;
|
|
2252
|
-
if (this.#advisors.length > 0) return true;
|
|
2253
|
-
if (!this.#advisorEnabled) return false;
|
|
2254
|
-
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
2255
|
-
|
|
2256
|
-
// The lone implicit "default" advisor (slug "") reproduces the legacy
|
|
2257
|
-
// single-advisor path: the `advisor` role model + `__advisor.jsonl`.
|
|
2283
|
+
#resolveAdvisorRuntimeDescriptors(emitWarnings: boolean): AdvisorRuntimeDescriptor[] {
|
|
2258
2284
|
const legacy = !this.#advisorConfigs?.length;
|
|
2259
2285
|
const roster: AdvisorConfig[] = legacy ? [{ name: "default" }] : this.#advisorConfigs!;
|
|
2260
|
-
|
|
2261
|
-
// Advisor service tier (`tier.advisor`): "none" (default) runs the advisor
|
|
2262
|
-
// on standard processing; "inherit" tracks the session's live per-family
|
|
2263
|
-
// tiers per request (like the main agent, including /fast toggles); a
|
|
2264
|
-
// concrete value is broadcast across families and applied to the advisor
|
|
2265
|
-
// model's family. One value for all advisors.
|
|
2266
|
-
const advisorTierSetting = this.settings.get("tier.advisor");
|
|
2267
|
-
const advisorTierMap =
|
|
2268
|
-
advisorTierSetting === "inherit"
|
|
2269
|
-
? undefined
|
|
2270
|
-
: serviceTierForAllFamilies(serviceTierSettingToTier(advisorTierSetting));
|
|
2271
|
-
const advisorServiceTierResolver = (model: Model): ServiceTier | undefined =>
|
|
2272
|
-
advisorTierSetting === "inherit"
|
|
2273
|
-
? this.#effectiveServiceTier(model)
|
|
2274
|
-
: resolveModelServiceTier(advisorTierMap, model);
|
|
2275
|
-
|
|
2286
|
+
const descriptors: AdvisorRuntimeDescriptor[] = [];
|
|
2276
2287
|
const usedSlugs = new Set<string>();
|
|
2277
2288
|
for (const config of roster) {
|
|
2278
2289
|
let slug = legacy ? "" : slugifyAdvisorName(config.name);
|
|
@@ -2293,23 +2304,84 @@ export class AgentSession {
|
|
|
2293
2304
|
model = resolved.model;
|
|
2294
2305
|
thinkingLevel = concreteThinkingLevel(resolved.thinkingLevel);
|
|
2295
2306
|
if (!model) {
|
|
2296
|
-
|
|
2307
|
+
if (emitWarnings) {
|
|
2308
|
+
this.emitNotice("warning", `Advisor "${config.name}": no model matched "${config.model}"`, "advisor");
|
|
2309
|
+
}
|
|
2297
2310
|
continue;
|
|
2298
2311
|
}
|
|
2299
2312
|
} else {
|
|
2300
2313
|
const sel = resolveAdvisorRoleSelection(this.settings, this.#modelRegistry.getAvailable());
|
|
2301
2314
|
if (!sel) {
|
|
2302
|
-
|
|
2303
|
-
advisor
|
|
2304
|
-
|
|
2315
|
+
if (emitWarnings) {
|
|
2316
|
+
logger.debug("advisor enabled but no model assigned to the 'advisor' role; advisor inactive", {
|
|
2317
|
+
advisor: config.name,
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2305
2320
|
continue;
|
|
2306
2321
|
}
|
|
2307
2322
|
model = sel.model;
|
|
2308
2323
|
thinkingLevel = concreteThinkingLevel(sel.thinkingLevel);
|
|
2309
2324
|
}
|
|
2310
|
-
const advisorModel = model;
|
|
2311
|
-
const advisorName = config.name;
|
|
2312
2325
|
const advisorThinkingLevel = thinkingLevel ?? ThinkingLevel.Medium;
|
|
2326
|
+
descriptors.push({
|
|
2327
|
+
config,
|
|
2328
|
+
name: config.name,
|
|
2329
|
+
slug,
|
|
2330
|
+
model,
|
|
2331
|
+
thinkingLevel: advisorThinkingLevel,
|
|
2332
|
+
signature: this.#advisorRuntimeSignature(config, slug, model, advisorThinkingLevel),
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
return descriptors;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2338
|
+
#advisorRuntimeSignature(config: AdvisorConfig, slug: string, model: Model, thinkingLevel: ThinkingLevel): string {
|
|
2339
|
+
const tools = config.tools?.length ? config.tools.join("\u001e") : "";
|
|
2340
|
+
const instructions = config.instructions?.trim() ?? "";
|
|
2341
|
+
return [config.name, slug, model.provider, model.id, thinkingLevel, tools, instructions].join("\u001f");
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2344
|
+
#advisorRuntimeMatchesCurrentConfig(): boolean {
|
|
2345
|
+
const descriptors = this.#resolveAdvisorRuntimeDescriptors(false);
|
|
2346
|
+
if (descriptors.length !== this.#advisors.length) return false;
|
|
2347
|
+
for (let i = 0; i < descriptors.length; i++) {
|
|
2348
|
+
if (descriptors[i].signature !== this.#advisors[i].signature) return false;
|
|
2349
|
+
}
|
|
2350
|
+
return true;
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
#buildAdvisorRuntime(seedToCurrent = false): boolean {
|
|
2354
|
+
if (this.#isDisposed) return false;
|
|
2355
|
+
if (this.#advisors.length > 0) return true;
|
|
2356
|
+
if (!this.#advisorEnabled) return false;
|
|
2357
|
+
if (this.#agentKind !== "main" && !this.settings.get("advisor.subagents")) return false;
|
|
2358
|
+
|
|
2359
|
+
const descriptors = this.#resolveAdvisorRuntimeDescriptors(true);
|
|
2360
|
+
|
|
2361
|
+
// Advisor service tier (`tier.advisor`): "none" (default) runs the advisor
|
|
2362
|
+
// on standard processing; "inherit" tracks the session's live per-family
|
|
2363
|
+
// tiers per request (like the main agent, including /fast toggles); a
|
|
2364
|
+
// concrete value is broadcast across families and applied to the advisor
|
|
2365
|
+
// model's family. One value for all advisors.
|
|
2366
|
+
const advisorTierSetting = this.settings.get("tier.advisor");
|
|
2367
|
+
const advisorTierMap =
|
|
2368
|
+
advisorTierSetting === "inherit"
|
|
2369
|
+
? undefined
|
|
2370
|
+
: serviceTierForAllFamilies(serviceTierSettingToTier(advisorTierSetting));
|
|
2371
|
+
const advisorServiceTierResolver = (model: Model): ServiceTier | undefined =>
|
|
2372
|
+
advisorTierSetting === "inherit"
|
|
2373
|
+
? this.#effectiveServiceTier(model)
|
|
2374
|
+
: resolveModelServiceTier(advisorTierMap, model);
|
|
2375
|
+
|
|
2376
|
+
for (const descriptor of descriptors) {
|
|
2377
|
+
const {
|
|
2378
|
+
config,
|
|
2379
|
+
slug,
|
|
2380
|
+
model: advisorModel,
|
|
2381
|
+
name: advisorName,
|
|
2382
|
+
thinkingLevel: advisorThinkingLevel,
|
|
2383
|
+
signature,
|
|
2384
|
+
} = descriptor;
|
|
2313
2385
|
|
|
2314
2386
|
const emissionGuard = new AdvisorEmissionGuard();
|
|
2315
2387
|
const adviseTool = new AdviseTool((note, severity) => this.#routeAdvice(advisorRef, note, severity));
|
|
@@ -2435,6 +2507,7 @@ export class AgentSession {
|
|
|
2435
2507
|
recorderClosed: Promise.resolve(),
|
|
2436
2508
|
model: advisorModel,
|
|
2437
2509
|
thinkingLevel: advisorThinkingLevel,
|
|
2510
|
+
signature,
|
|
2438
2511
|
};
|
|
2439
2512
|
this.#attachAdvisorRecorderFeed(advisorRef);
|
|
2440
2513
|
if (seedToCurrent) runtime.seedTo(this.agent.state.messages.length);
|
|
@@ -2521,6 +2594,20 @@ export class AgentSession {
|
|
|
2521
2594
|
return;
|
|
2522
2595
|
}
|
|
2523
2596
|
this.#recordAdvisorInterruptDelivered();
|
|
2597
|
+
if (this.#planModeState?.enabled) {
|
|
2598
|
+
// Plan mode: record advice visibly in context but never wake an
|
|
2599
|
+
// autonomous turn — only user-driven turns converge on ask/resolve.
|
|
2600
|
+
this.#preserveAdvisorCard({
|
|
2601
|
+
role: "custom",
|
|
2602
|
+
customType: "advisor",
|
|
2603
|
+
content,
|
|
2604
|
+
display: true,
|
|
2605
|
+
attribution: "agent",
|
|
2606
|
+
details,
|
|
2607
|
+
timestamp: Date.now(),
|
|
2608
|
+
});
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
2524
2611
|
void this.sendCustomMessage(
|
|
2525
2612
|
{ customType: "advisor", content, display: true, attribution: "agent", details },
|
|
2526
2613
|
{ deliverAs: "steer", triggerTurn: true },
|
|
@@ -2945,6 +3032,26 @@ export class AgentSession {
|
|
|
2945
3032
|
manager?.cancelAll({ ownerId: this.#agentId });
|
|
2946
3033
|
}
|
|
2947
3034
|
|
|
3035
|
+
/**
|
|
3036
|
+
* True when a background async job owned by this agent is still running with
|
|
3037
|
+
* an unsuppressed delivery, or a finished job's delivery is still queued or
|
|
3038
|
+
* in flight. Either way the async-result follow-up will re-wake the loop, so
|
|
3039
|
+
* a settle observed now is a scheduling pause rather than a terminal stop:
|
|
3040
|
+
* stop-time passes (todo reminder, session_stop hooks) defer to the settle
|
|
3041
|
+
* reached once the session is fully idle. Suppressed deliveries
|
|
3042
|
+
* (acknowledged, or watched by an in-flight `job` poll) never wake the loop,
|
|
3043
|
+
* so they don't count.
|
|
3044
|
+
*/
|
|
3045
|
+
#hasPendingAsyncWake(): boolean {
|
|
3046
|
+
const manager = this.#asyncJobManager;
|
|
3047
|
+
if (!manager) return false;
|
|
3048
|
+
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
3049
|
+
return (
|
|
3050
|
+
manager.getRunningJobs(ownerFilter).some(job => !manager.isDeliverySuppressed(job.id)) ||
|
|
3051
|
+
manager.hasPendingDeliveries(ownerFilter)
|
|
3052
|
+
);
|
|
3053
|
+
}
|
|
3054
|
+
|
|
2948
3055
|
// =========================================================================
|
|
2949
3056
|
// Event Subscription
|
|
2950
3057
|
// =========================================================================
|
|
@@ -3421,6 +3528,11 @@ export class AgentSession {
|
|
|
3421
3528
|
} else {
|
|
3422
3529
|
await this.#goalRuntime.onToolCompleted(event.toolName);
|
|
3423
3530
|
}
|
|
3531
|
+
this.#planModeReminderAwaitingProgress = false;
|
|
3532
|
+
if (this.#isPlanDecisionTool(event.toolName)) {
|
|
3533
|
+
this.#planModeReminderCount = 0;
|
|
3534
|
+
this.#planModeReminderAwaitingProgress = false;
|
|
3535
|
+
}
|
|
3424
3536
|
}
|
|
3425
3537
|
if (event.type === "tool_execution_end" && event.toolName === "yield" && !event.isError) {
|
|
3426
3538
|
this.#lastSuccessfulYieldToolCallId = event.toolCallId;
|
|
@@ -3843,12 +3955,26 @@ export class AgentSession {
|
|
|
3843
3955
|
await emitAgentEndNotification();
|
|
3844
3956
|
return;
|
|
3845
3957
|
}
|
|
3958
|
+
const planModeContinuationScheduled = await this.#enforcePlanModeDecisionAtSettle();
|
|
3959
|
+
if (planModeContinuationScheduled) {
|
|
3960
|
+
await emitAgentEndNotification();
|
|
3961
|
+
return;
|
|
3962
|
+
}
|
|
3846
3963
|
const todoContinuationScheduled = await this.#checkTodoCompletion();
|
|
3847
3964
|
if (todoContinuationScheduled) {
|
|
3848
3965
|
await emitAgentEndNotification();
|
|
3849
3966
|
return;
|
|
3850
3967
|
}
|
|
3851
3968
|
}
|
|
3969
|
+
// A pending async wake means this settle is a scheduling pause, not
|
|
3970
|
+
// the terminal stop: the async-result delivery continues the loop and
|
|
3971
|
+
// the real stop settles later. Defer the session_stop hook pass until
|
|
3972
|
+
// the session is fully idle (the todo reminder above defers the same
|
|
3973
|
+
// way inside #checkTodoCompletion).
|
|
3974
|
+
if (this.#hasPendingAsyncWake()) {
|
|
3975
|
+
await emitAgentEndNotification();
|
|
3976
|
+
return;
|
|
3977
|
+
}
|
|
3852
3978
|
await this.#emitSessionStopEvent(settledMessages, msg);
|
|
3853
3979
|
await emitAgentEndNotification();
|
|
3854
3980
|
}
|
|
@@ -6684,6 +6810,12 @@ export class AgentSession {
|
|
|
6684
6810
|
if (state?.enabled) {
|
|
6685
6811
|
this.#planReferenceSent = false;
|
|
6686
6812
|
this.#planReferencePath = state.planFilePath;
|
|
6813
|
+
} else {
|
|
6814
|
+
this.#planModeReminderCount = 0;
|
|
6815
|
+
this.#planModeReminderAwaitingProgress = false;
|
|
6816
|
+
// Drop any unconsumed forced decision so a post-plan execution turn
|
|
6817
|
+
// does not inherit a stale `required` tool choice.
|
|
6818
|
+
this.#toolChoiceQueue.removeByLabel("plan-mode-decision");
|
|
6687
6819
|
}
|
|
6688
6820
|
}
|
|
6689
6821
|
|
|
@@ -6948,15 +7080,30 @@ export class AgentSession {
|
|
|
6948
7080
|
return {
|
|
6949
7081
|
role: "custom",
|
|
6950
7082
|
customType: "goal-mode-context",
|
|
6951
|
-
content:
|
|
7083
|
+
content: prompt.render(goalModeContextPrompt, { goalContext: content, todoContext }),
|
|
6952
7084
|
display: false,
|
|
6953
7085
|
attribution: "agent",
|
|
6954
7086
|
timestamp: Date.now(),
|
|
6955
7087
|
};
|
|
6956
7088
|
}
|
|
6957
7089
|
|
|
7090
|
+
#sanitizeGoalTodoText(text: string): string {
|
|
7091
|
+
return escapeXmlText(text)
|
|
7092
|
+
.replace(/\r\n/g, "\\n")
|
|
7093
|
+
.replace(/\r/g, "\\r")
|
|
7094
|
+
.replace(/\n/g, "\\n")
|
|
7095
|
+
.replace(/\t/g, "\\t")
|
|
7096
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u2028\u2029]/g, " ");
|
|
7097
|
+
}
|
|
7098
|
+
|
|
6958
7099
|
#buildGoalTodoContext(): string | undefined {
|
|
6959
7100
|
if (!this.settings.get("todo.enabled")) return undefined;
|
|
7101
|
+
const activeToolNames = this.getActiveToolNames();
|
|
7102
|
+
const canCallTodoTool = activeToolNames.includes("todo");
|
|
7103
|
+
const canDiscoverTodoTool =
|
|
7104
|
+
!canCallTodoTool && this.getDiscoverableTools({ source: "builtin" }).some(tool => tool.name === "todo");
|
|
7105
|
+
const canActivateTodoTool = canDiscoverTodoTool && activeToolNames.includes("search_tool_bm25");
|
|
7106
|
+
if (!canCallTodoTool && !canDiscoverTodoTool) return undefined;
|
|
6960
7107
|
const phases = this.getTodoPhases().filter(phase => phase.tasks.length > 0);
|
|
6961
7108
|
if (phases.length === 0) return undefined;
|
|
6962
7109
|
|
|
@@ -6964,7 +7111,7 @@ export class AgentSession {
|
|
|
6964
7111
|
let closed = 0;
|
|
6965
7112
|
let open = 0;
|
|
6966
7113
|
const promptPhases = phases.map(phase => ({
|
|
6967
|
-
name: phase.name,
|
|
7114
|
+
name: this.#sanitizeGoalTodoText(phase.name),
|
|
6968
7115
|
tasks: phase.tasks.map(task => {
|
|
6969
7116
|
total++;
|
|
6970
7117
|
if (task.status === "completed" || task.status === "abandoned") {
|
|
@@ -6972,11 +7119,13 @@ export class AgentSession {
|
|
|
6972
7119
|
} else {
|
|
6973
7120
|
open++;
|
|
6974
7121
|
}
|
|
6975
|
-
return { content: task.content, status: task.status };
|
|
7122
|
+
return { content: this.#sanitizeGoalTodoText(task.content), status: task.status };
|
|
6976
7123
|
}),
|
|
6977
7124
|
}));
|
|
6978
7125
|
|
|
6979
7126
|
return prompt.render(goalTodoContextPrompt, {
|
|
7127
|
+
canCallTodoTool,
|
|
7128
|
+
canActivateTodoTool,
|
|
6980
7129
|
closed: String(closed),
|
|
6981
7130
|
open: String(open),
|
|
6982
7131
|
phases: promptPhases,
|
|
@@ -7162,6 +7311,11 @@ export class AgentSession {
|
|
|
7162
7311
|
// Agent-initiated synthetic prompts (auto-continue, plan, reminders) do not.
|
|
7163
7312
|
if (options?.userInitiated ?? !options?.synthetic) {
|
|
7164
7313
|
this.#advisorAutoResumeSuppressed = false;
|
|
7314
|
+
this.#planModeReminderCount = 0;
|
|
7315
|
+
this.#planModeReminderAwaitingProgress = false;
|
|
7316
|
+
// A user turn owns the next decision; drop a queued forced choice from
|
|
7317
|
+
// a reminder continuation this prompt just preempted.
|
|
7318
|
+
this.#toolChoiceQueue.removeByLabel("plan-mode-decision");
|
|
7165
7319
|
}
|
|
7166
7320
|
|
|
7167
7321
|
// If streaming, queue via steer() or followUp() based on option
|
|
@@ -7232,9 +7386,6 @@ export class AgentSession {
|
|
|
7232
7386
|
// (e.g., compaction aborted, validation failed).
|
|
7233
7387
|
this.#toolChoiceQueue.removeByLabel("eager-todo");
|
|
7234
7388
|
}
|
|
7235
|
-
if (!options?.synthetic) {
|
|
7236
|
-
await this.#enforcePlanModeToolDecision();
|
|
7237
|
-
}
|
|
7238
7389
|
return true;
|
|
7239
7390
|
}
|
|
7240
7391
|
|
|
@@ -8483,12 +8634,10 @@ export class AgentSession {
|
|
|
8483
8634
|
/**
|
|
8484
8635
|
* Set model directly.
|
|
8485
8636
|
* Validates that a credential source is configured (synchronously, without
|
|
8486
|
-
* refreshing OAuth or running command-backed key programs).
|
|
8487
|
-
*
|
|
8488
|
-
*
|
|
8489
|
-
*
|
|
8490
|
-
* model actually switched, computed against the refreshed metadata so
|
|
8491
|
-
* dynamic providers (e.g. llama.cpp) honor their post-load contextWindow.
|
|
8637
|
+
* refreshing OAuth or running command-backed key programs). Active switches
|
|
8638
|
+
* always take effect; if the current transcript is too large for the target
|
|
8639
|
+
* model, the next prompt's compaction/error path owns that recovery instead
|
|
8640
|
+
* of leaving the session pinned to the old model.
|
|
8492
8641
|
* @throws Error if no API key available for the model
|
|
8493
8642
|
*/
|
|
8494
8643
|
async setModel(
|
|
@@ -8508,28 +8657,15 @@ export class AgentSession {
|
|
|
8508
8657
|
|
|
8509
8658
|
const targetModel = await this.#modelRegistry.refreshSelectedModelMetadata(model);
|
|
8510
8659
|
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
currentContextTokens > 0 &&
|
|
8515
|
-
targetContextWindow > 0 &&
|
|
8516
|
-
currentContextTokens > targetContextWindow
|
|
8517
|
-
);
|
|
8518
|
-
|
|
8519
|
-
if (switched) {
|
|
8520
|
-
this.#clearActiveRetryFallback();
|
|
8521
|
-
this.#setModelWithProviderSessionReset(targetModel);
|
|
8522
|
-
this.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
|
|
8523
|
-
}
|
|
8660
|
+
this.#clearActiveRetryFallback();
|
|
8661
|
+
this.#setModelWithProviderSessionReset(targetModel);
|
|
8662
|
+
this.sessionManager.appendModelChange(`${targetModel.provider}/${targetModel.id}`, role);
|
|
8524
8663
|
if (options?.persist) {
|
|
8525
8664
|
this.settings.setModelRole(
|
|
8526
8665
|
role,
|
|
8527
8666
|
this.#formatRoleModelValue(role, targetModel, options.selector, options.thinkingLevel),
|
|
8528
8667
|
);
|
|
8529
8668
|
}
|
|
8530
|
-
if (!switched) {
|
|
8531
|
-
return { switched: false };
|
|
8532
|
-
}
|
|
8533
8669
|
this.settings.getStorage()?.recordModelUsage(`${targetModel.provider}/${targetModel.id}`);
|
|
8534
8670
|
|
|
8535
8671
|
// Re-apply thinking for the newly selected model. Prefer the model's
|
|
@@ -10062,6 +10198,48 @@ export class AgentSession {
|
|
|
10062
10198
|
}
|
|
10063
10199
|
return COMPACTION_CHECK_NONE;
|
|
10064
10200
|
}
|
|
10201
|
+
// A context promotion can land while the failing call is already in
|
|
10202
|
+
// flight (or on a run whose loop predates the switch): the overflow
|
|
10203
|
+
// error then arrives stamped with the pre-promotion model while
|
|
10204
|
+
// `this.model` is already the promoted target. The sameModel guard
|
|
10205
|
+
// above deliberately ignores stale foreign-model errors, but this
|
|
10206
|
+
// state is not stale — recover exactly like the promotion path:
|
|
10207
|
+
// drop the dead turn and retry on the already-promoted model. Gated
|
|
10208
|
+
// narrowly on "current model IS the failed model's promotion target
|
|
10209
|
+
// with a strictly larger window" so genuinely stale errors from
|
|
10210
|
+
// old user-switched models keep surfacing untouched.
|
|
10211
|
+
if (
|
|
10212
|
+
!sameModel &&
|
|
10213
|
+
autoContinue &&
|
|
10214
|
+
!errorIsFromBeforeCompaction &&
|
|
10215
|
+
assistantMessage.stopReason === "error" &&
|
|
10216
|
+
this.model &&
|
|
10217
|
+
contextWindow > 0 &&
|
|
10218
|
+
this.settings.getGroup("contextPromotion").enabled
|
|
10219
|
+
) {
|
|
10220
|
+
const failedModel = this.#modelRegistry.find(assistantMessage.provider, assistantMessage.model);
|
|
10221
|
+
const failedWindow = failedModel?.contextWindow ?? 0;
|
|
10222
|
+
const promotionTarget = failedModel
|
|
10223
|
+
? this.#resolveContextPromotionConfiguredTarget(failedModel, this.#modelRegistry.getAvailable())
|
|
10224
|
+
: undefined;
|
|
10225
|
+
if (
|
|
10226
|
+
failedModel &&
|
|
10227
|
+
failedWindow > 0 &&
|
|
10228
|
+
contextWindow > failedWindow &&
|
|
10229
|
+
promotionTarget &&
|
|
10230
|
+
modelsAreEqual(promotionTarget, this.model) &&
|
|
10231
|
+
AIError.isContextOverflow(assistantMessage, failedWindow)
|
|
10232
|
+
) {
|
|
10233
|
+
this.#removeAssistantMessageFromActiveContext(assistantMessage);
|
|
10234
|
+
await this.#dropPersistedAssistantTurn(assistantMessage);
|
|
10235
|
+
logger.debug("Overflow on pre-promotion model; retrying on promoted model", {
|
|
10236
|
+
failed: `${assistantMessage.provider}/${assistantMessage.model}`,
|
|
10237
|
+
current: `${this.model.provider}/${this.model.id}`,
|
|
10238
|
+
});
|
|
10239
|
+
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
10240
|
+
return COMPACTION_CHECK_CONTINUATION;
|
|
10241
|
+
}
|
|
10242
|
+
}
|
|
10065
10243
|
|
|
10066
10244
|
// Case 3: Output-side incomplete — `response.incomplete` from OpenAI Responses
|
|
10067
10245
|
// (and Codex) maps to stopReason === "length". The model burned its
|
|
@@ -10548,41 +10726,72 @@ export class AgentSession {
|
|
|
10548
10726
|
this.#checkpointState = undefined;
|
|
10549
10727
|
this.#pendingRewindReport = undefined;
|
|
10550
10728
|
}
|
|
10551
|
-
|
|
10729
|
+
#isPlanDecisionTool(name: string): boolean {
|
|
10730
|
+
return PLAN_DECISION_TOOLS.has(name);
|
|
10731
|
+
}
|
|
10732
|
+
|
|
10733
|
+
async #enforcePlanModeDecisionAtSettle(): Promise<boolean> {
|
|
10552
10734
|
if (!this.#planModeState?.enabled) {
|
|
10553
|
-
return;
|
|
10735
|
+
return false;
|
|
10554
10736
|
}
|
|
10555
10737
|
const assistantMessage = this.#findLastAssistantMessage();
|
|
10556
10738
|
if (!assistantMessage) {
|
|
10557
|
-
return;
|
|
10739
|
+
return false;
|
|
10558
10740
|
}
|
|
10559
10741
|
if (assistantMessage.stopReason === "error" || assistantMessage.stopReason === "aborted") {
|
|
10560
|
-
return;
|
|
10742
|
+
return false;
|
|
10561
10743
|
}
|
|
10562
10744
|
|
|
10563
|
-
const
|
|
10564
|
-
content => content.type === "toolCall" && (content.name
|
|
10745
|
+
const calledDecisionTool = assistantMessage.content.some(
|
|
10746
|
+
content => content.type === "toolCall" && this.#isPlanDecisionTool(content.name),
|
|
10565
10747
|
);
|
|
10566
|
-
if (
|
|
10567
|
-
|
|
10748
|
+
if (calledDecisionTool) {
|
|
10749
|
+
this.#planModeReminderCount = 0;
|
|
10750
|
+
this.#planModeReminderAwaitingProgress = false;
|
|
10751
|
+
return false;
|
|
10752
|
+
}
|
|
10753
|
+
|
|
10754
|
+
const hasToolCall = assistantMessage.content.some(content => content.type === "toolCall");
|
|
10755
|
+
if (hasToolCall) {
|
|
10756
|
+
return false;
|
|
10757
|
+
}
|
|
10758
|
+
if (this.#planModeReminderAwaitingProgress) {
|
|
10759
|
+
return false;
|
|
10760
|
+
}
|
|
10761
|
+
if (this.#planModeReminderCount >= PLAN_MODE_REMINDER_MAX) {
|
|
10762
|
+
logger.debug("Plan mode convergence: reminder cap reached; yielding to user");
|
|
10763
|
+
return false;
|
|
10568
10764
|
}
|
|
10569
10765
|
const hasRequiredTools = this.#toolRegistry.has("ask") && this.#toolRegistry.has("resolve");
|
|
10570
10766
|
if (!hasRequiredTools) {
|
|
10571
10767
|
logger.warn("Plan mode enforcement skipped because ask/resolve tools are unavailable", {
|
|
10572
10768
|
activeToolNames: this.agent.state.tools.map(tool => tool.name),
|
|
10573
10769
|
});
|
|
10574
|
-
return;
|
|
10770
|
+
return false;
|
|
10575
10771
|
}
|
|
10576
10772
|
|
|
10773
|
+
this.#planModeReminderCount++;
|
|
10774
|
+
this.#planModeReminderAwaitingProgress = true;
|
|
10775
|
+
this.#toolChoiceQueue.pushOnce("required", { label: "plan-mode-decision" });
|
|
10577
10776
|
const reminder = prompt.render(planModeToolDecisionReminderPrompt, {
|
|
10578
10777
|
askToolName: "ask",
|
|
10579
10778
|
});
|
|
10779
|
+
const reminderMessage: Message = {
|
|
10780
|
+
role: "developer",
|
|
10781
|
+
content: [{ type: "text", text: reminder }],
|
|
10782
|
+
attribution: "agent",
|
|
10783
|
+
timestamp: Date.now(),
|
|
10784
|
+
};
|
|
10580
10785
|
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10786
|
+
this.agent.appendMessage(reminderMessage);
|
|
10787
|
+
this.sessionManager.appendMessage(reminderMessage);
|
|
10788
|
+
this.#scheduleAgentContinue({
|
|
10789
|
+
generation: this.#promptGeneration,
|
|
10790
|
+
// If the continuation never runs (new prompt, dispose, compaction,
|
|
10791
|
+
// handoff), the forced choice must not leak onto an unrelated turn.
|
|
10792
|
+
onSkip: () => this.#toolChoiceQueue.removeByLabel("plan-mode-decision"),
|
|
10585
10793
|
});
|
|
10794
|
+
return true;
|
|
10586
10795
|
}
|
|
10587
10796
|
|
|
10588
10797
|
/**
|
|
@@ -10731,6 +10940,13 @@ export class AgentSession {
|
|
|
10731
10940
|
return false;
|
|
10732
10941
|
}
|
|
10733
10942
|
|
|
10943
|
+
// Plan mode owns convergence via #enforcePlanModeDecisionAtSettle (remind →
|
|
10944
|
+
// cap → yield). Todo reminders must not re-wake a turn the cap intends to
|
|
10945
|
+
// yield to the user. The label is already consumed above, so no leak.
|
|
10946
|
+
if (this.#planModeState?.enabled) {
|
|
10947
|
+
return false;
|
|
10948
|
+
}
|
|
10949
|
+
|
|
10734
10950
|
// Suppress within a self-continuation chain: if the agent's last turn was driven by a
|
|
10735
10951
|
// prior reminder (and the agent took no tool-level action since), do not re-ping.
|
|
10736
10952
|
// The agent has already acknowledged; further escalation just wastes context and
|
|
@@ -10781,6 +10997,18 @@ export class AgentSession {
|
|
|
10781
10997
|
return false;
|
|
10782
10998
|
}
|
|
10783
10999
|
|
|
11000
|
+
// Background async jobs (bash/task) owned by this agent re-wake the loop
|
|
11001
|
+
// when they complete: the result delivery enqueues an async-result
|
|
11002
|
+
// follow-up that continues the run, and todos are re-evaluated at that
|
|
11003
|
+
// settle. A stop with such a job in flight is a scheduling pause, not
|
|
11004
|
+
// abandonment — stay silent instead of nagging.
|
|
11005
|
+
if (this.#hasPendingAsyncWake()) {
|
|
11006
|
+
logger.debug("Todo completion: async jobs in flight will re-wake the loop; skipping reminder", {
|
|
11007
|
+
incomplete: incomplete.length,
|
|
11008
|
+
});
|
|
11009
|
+
return false;
|
|
11010
|
+
}
|
|
11011
|
+
|
|
10784
11012
|
// Build reminder message
|
|
10785
11013
|
this.#todoReminderCount++;
|
|
10786
11014
|
const todoList = incompleteByPhase
|
|
@@ -13676,24 +13904,32 @@ export class AgentSession {
|
|
|
13676
13904
|
*
|
|
13677
13905
|
* - mid-turn → queued on the aside channel and folded in at the next step
|
|
13678
13906
|
* boundary (non-interrupting, like async-result deliveries) → "injected";
|
|
13907
|
+
* - idle in plan mode → appended into context without waking an autonomous
|
|
13908
|
+
* turn (convergence stays user-driven) → "injected";
|
|
13679
13909
|
* - idle → starts a real turn with the message so the recipient wakes
|
|
13680
13910
|
* → "woken".
|
|
13681
13911
|
*
|
|
13682
13912
|
* Never blocks on the recipient's turn: the wake turn is fire-and-forget.
|
|
13683
13913
|
*
|
|
13684
|
-
* When the sender expects a reply (`send await:true`) and this session
|
|
13685
|
-
*
|
|
13686
|
-
* gated on the sender's own batch
|
|
13687
|
-
*
|
|
13688
|
-
* side-channel auto-reply is generated from the current context
|
|
13689
|
-
* `respondAsBackground` path) and sent back over the bus on this
|
|
13690
|
-
* behalf.
|
|
13914
|
+
* When the sender expects a reply (`send await:true`) and this session
|
|
13915
|
+
* cannot produce a real reply turn in time — mid-turn with async execution
|
|
13916
|
+
* disabled (the next step boundary may be gated on the sender's own batch
|
|
13917
|
+
* finishing), or idle in plan mode (wake turns are suppressed) — an
|
|
13918
|
+
* ephemeral side-channel auto-reply is generated from the current context
|
|
13919
|
+
* (the old `respondAsBackground` path) and sent back over the bus on this
|
|
13920
|
+
* agent's behalf.
|
|
13691
13921
|
*/
|
|
13692
13922
|
async deliverIrcMessage(msg: IrcMessage, opts?: { expectsReply?: boolean }): Promise<"injected" | "woken"> {
|
|
13693
13923
|
if (this.#isDisposed) {
|
|
13694
13924
|
throw new Error("Recipient session is disposed.");
|
|
13695
13925
|
}
|
|
13696
|
-
|
|
13926
|
+
// Auto-reply eligibility: the sender is blocked on an answer and this
|
|
13927
|
+
// session cannot produce a real reply turn in time — either mid-turn with
|
|
13928
|
+
// async execution disabled (no step boundary until the sender's own batch
|
|
13929
|
+
// ends), or idle in plan mode (autonomous wake turns are suppressed).
|
|
13930
|
+
const planModeIdle = !this.isStreaming && this.#planModeState?.enabled === true;
|
|
13931
|
+
const autoReply =
|
|
13932
|
+
(opts?.expectsReply ?? false) && ((this.isStreaming && !this.settings.get("async.enabled")) || planModeIdle);
|
|
13697
13933
|
const record: CustomMessage = {
|
|
13698
13934
|
role: "custom",
|
|
13699
13935
|
customType: "irc:incoming",
|
|
@@ -13726,6 +13962,19 @@ export class AgentSession {
|
|
|
13726
13962
|
if (autoReply) void this.#runIrcAutoReply(msg);
|
|
13727
13963
|
return "injected";
|
|
13728
13964
|
}
|
|
13965
|
+
// Plan mode: record into context but do not wake an autonomous turn.
|
|
13966
|
+
if (this.#planModeState?.enabled) {
|
|
13967
|
+
this.agent.appendMessage(record);
|
|
13968
|
+
this.sessionManager.appendCustomMessageEntry(
|
|
13969
|
+
record.customType,
|
|
13970
|
+
record.content,
|
|
13971
|
+
record.display,
|
|
13972
|
+
record.details,
|
|
13973
|
+
record.attribution ?? "agent",
|
|
13974
|
+
);
|
|
13975
|
+
if (autoReply) void this.#runIrcAutoReply(msg);
|
|
13976
|
+
return "injected";
|
|
13977
|
+
}
|
|
13729
13978
|
// Idle: wake a real turn so the recipient responds (shared with the stranded-aside resume).
|
|
13730
13979
|
this.#wakeForIrc([record]);
|
|
13731
13980
|
return "woken";
|
|
@@ -15187,6 +15436,7 @@ export class AgentSession {
|
|
|
15187
15436
|
setAdvisorEnabled(enabled: boolean): boolean {
|
|
15188
15437
|
this.#advisorEnabled = enabled;
|
|
15189
15438
|
if (enabled) {
|
|
15439
|
+
if (this.#advisors.length > 0 && !this.#advisorRuntimeMatchesCurrentConfig()) this.#stopAdvisorRuntime();
|
|
15190
15440
|
return this.#buildAdvisorRuntime(true);
|
|
15191
15441
|
}
|
|
15192
15442
|
this.#stopAdvisorRuntime();
|