@oh-my-pi/pi-coding-agent 16.0.0 → 16.0.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 +140 -133
- package/dist/cli.js +250 -218
- package/dist/types/config/model-resolver.d.ts +14 -0
- 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/exec/non-interactive-env.d.ts +2 -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/messages.d.ts +3 -0
- 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/utils/markit.d.ts +8 -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/advisor/__tests__/advisor.test.ts +44 -0
- package/src/cli/args.ts +2 -0
- package/src/collab/host.ts +1 -1
- package/src/config/model-resolver.ts +35 -1
- package/src/config/settings-schema.ts +23 -1
- package/src/discovery/claude-plugins.ts +3 -42
- package/src/discovery/github.ts +189 -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/exec/bash-executor.ts +2 -2
- package/src/exec/non-interactive-env.ts +71 -0
- package/src/extensibility/custom-commands/bundled/review/index.ts +289 -80
- package/src/extensibility/extensions/runner.ts +17 -1
- package/src/extensibility/plugins/loader.ts +157 -23
- package/src/extensibility/plugins/manager.ts +44 -36
- 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 +9 -9
- 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 +260 -50
- package/src/session/messages.ts +1 -1
- package/src/session/session-manager.ts +3 -1
- package/src/slash-commands/builtin-registry.ts +5 -2
- package/src/system-prompt.ts +7 -1
- package/src/task/executor.ts +105 -8
- package/src/task/index.ts +70 -9
- package/src/tools/github-cache.ts +32 -7
- package/src/tools/job.ts +14 -1
- package/src/utils/lang-from-path.ts +5 -0
- package/src/utils/markit.ts +24 -1
- package/src/web/search/index.ts +2 -2
- package/src/web/search/provider.ts +14 -2
|
@@ -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/system-prompt.ts
CHANGED
|
@@ -615,7 +615,13 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
615
615
|
resolvedCustomPrompt,
|
|
616
616
|
resolvedAppendPrompt,
|
|
617
617
|
]);
|
|
618
|
-
const
|
|
618
|
+
const contextPromptSources = contextFiles.map(file => file.content);
|
|
619
|
+
const promptSources = [
|
|
620
|
+
effectiveSystemPromptCustomization,
|
|
621
|
+
resolvedCustomPrompt,
|
|
622
|
+
resolvedAppendPrompt,
|
|
623
|
+
...contextPromptSources,
|
|
624
|
+
];
|
|
619
625
|
const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
|
|
620
626
|
|
|
621
627
|
const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
|
package/src/task/executor.ts
CHANGED
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import type { AgentEvent, AgentIdentity, AgentTelemetryConfig, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
9
9
|
import { recordHandoff, resolveTelemetry } from "@oh-my-pi/pi-agent-core";
|
|
10
|
-
import type { Usage } from "@oh-my-pi/pi-ai";
|
|
10
|
+
import type { Api, Model, Usage } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import { logger, popLoopPhase, prompt, pushLoopPhase, untilAborted } from "@oh-my-pi/pi-utils";
|
|
12
12
|
import type { Rule } from "../capability/rule";
|
|
13
13
|
import { ModelRegistry } from "../config/model-registry";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
formatModelSelectorValue,
|
|
16
|
+
formatModelStringWithRouting,
|
|
17
|
+
resolveModelOverride,
|
|
18
|
+
resolveModelOverrideWithAuthFallback,
|
|
19
|
+
} from "../config/model-resolver";
|
|
15
20
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
16
21
|
import { Settings } from "../config/settings";
|
|
17
22
|
import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
@@ -33,7 +38,7 @@ import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage
|
|
|
33
38
|
import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
34
39
|
import type { ArtifactManager } from "../session/artifacts";
|
|
35
40
|
import type { AuthStorage } from "../session/auth-storage";
|
|
36
|
-
import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
|
|
41
|
+
import { SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../session/messages";
|
|
37
42
|
import { SessionManager } from "../session/session-manager";
|
|
38
43
|
import { truncateTail } from "../session/streaming-output";
|
|
39
44
|
import type { ContextFileEntry } from "../tools";
|
|
@@ -120,6 +125,74 @@ function normalizeModelPatterns(value: string | string[] | undefined): string[]
|
|
|
120
125
|
.filter(Boolean);
|
|
121
126
|
}
|
|
122
127
|
|
|
128
|
+
const SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX = "subagent:";
|
|
129
|
+
|
|
130
|
+
interface SubagentRetryFallbackCandidate {
|
|
131
|
+
model: Model<Api>;
|
|
132
|
+
selector: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveSubagentRetryFallbackCandidates(
|
|
136
|
+
modelPatterns: string[],
|
|
137
|
+
modelRegistry: ModelRegistry,
|
|
138
|
+
settings: Settings,
|
|
139
|
+
): SubagentRetryFallbackCandidate[] {
|
|
140
|
+
const candidates: SubagentRetryFallbackCandidate[] = [];
|
|
141
|
+
const seen = new Set<string>();
|
|
142
|
+
for (const pattern of modelPatterns) {
|
|
143
|
+
const resolved = resolveModelOverride([pattern], modelRegistry, settings);
|
|
144
|
+
if (!resolved.model) continue;
|
|
145
|
+
const selector = resolved.explicitThinkingLevel
|
|
146
|
+
? formatModelSelectorValue(formatModelStringWithRouting(resolved.model), resolved.thinkingLevel)
|
|
147
|
+
: formatModelStringWithRouting(resolved.model);
|
|
148
|
+
if (seen.has(selector)) continue;
|
|
149
|
+
seen.add(selector);
|
|
150
|
+
candidates.push({ model: resolved.model, selector });
|
|
151
|
+
}
|
|
152
|
+
return candidates;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function installSubagentRetryFallbackChain(args: {
|
|
156
|
+
settings: Settings;
|
|
157
|
+
id: string;
|
|
158
|
+
candidates: SubagentRetryFallbackCandidate[];
|
|
159
|
+
model: Model<Api> | undefined;
|
|
160
|
+
authFallbackUsed: boolean;
|
|
161
|
+
}): string | undefined {
|
|
162
|
+
const { settings, id, candidates, model, authFallbackUsed } = args;
|
|
163
|
+
if (!model || authFallbackUsed || candidates.length <= 1) return undefined;
|
|
164
|
+
|
|
165
|
+
const selectedIndex = candidates.findIndex(
|
|
166
|
+
candidate => candidate.model.provider === model.provider && candidate.model.id === model.id,
|
|
167
|
+
);
|
|
168
|
+
if (selectedIndex < 0) return undefined;
|
|
169
|
+
const fallbackSelectors = candidates.slice(selectedIndex + 1).map(candidate => candidate.selector);
|
|
170
|
+
if (fallbackSelectors.length === 0) return undefined;
|
|
171
|
+
|
|
172
|
+
const role = `${SUBAGENT_RETRY_FALLBACK_ROLE_PREFIX}${id}`;
|
|
173
|
+
const modelRoles: Record<string, string> = {};
|
|
174
|
+
const existingRoles = settings.getModelRoles();
|
|
175
|
+
for (const existingRole in existingRoles) {
|
|
176
|
+
const selector = existingRoles[existingRole];
|
|
177
|
+
if (selector) {
|
|
178
|
+
modelRoles[existingRole] = selector;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
modelRoles[role] = candidates[selectedIndex].selector;
|
|
182
|
+
settings.override("modelRoles", modelRoles);
|
|
183
|
+
const fallbackChains: Record<string, string[]> = {
|
|
184
|
+
[role]: fallbackSelectors,
|
|
185
|
+
};
|
|
186
|
+
const existingFallbackChains = settings.get("retry.fallbackChains");
|
|
187
|
+
for (const existingRole in existingFallbackChains) {
|
|
188
|
+
if (existingRole !== role) {
|
|
189
|
+
fallbackChains[existingRole] = existingFallbackChains[existingRole];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
settings.override("retry.fallbackChains", fallbackChains);
|
|
193
|
+
return role;
|
|
194
|
+
}
|
|
195
|
+
|
|
123
196
|
function renderIrcPeerRoster(selfId: string): string {
|
|
124
197
|
const peers = AgentRegistry.global()
|
|
125
198
|
.list()
|
|
@@ -1222,6 +1295,16 @@ function createSubagentRunMonitor(args: RunMonitorArgs): SubagentRunMonitor {
|
|
|
1222
1295
|
popLoopPhase();
|
|
1223
1296
|
}
|
|
1224
1297
|
}
|
|
1298
|
+
if (event.type === "retry_fallback_applied") {
|
|
1299
|
+
progress.resolvedModel = event.to;
|
|
1300
|
+
scheduleProgress(true);
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
if (event.type === "retry_fallback_succeeded") {
|
|
1304
|
+
progress.resolvedModel = event.model;
|
|
1305
|
+
scheduleProgress(true);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1225
1308
|
});
|
|
1226
1309
|
|
|
1227
1310
|
const captureSalvage = (session: AgentSession): void => {
|
|
@@ -1817,21 +1900,35 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1817
1900
|
resolvedModel: model.id,
|
|
1818
1901
|
});
|
|
1819
1902
|
}
|
|
1903
|
+
const retryFallbackRole = installSubagentRetryFallbackChain({
|
|
1904
|
+
settings: subagentSettings,
|
|
1905
|
+
id,
|
|
1906
|
+
candidates: resolveSubagentRetryFallbackCandidates(modelPatterns, modelRegistry, settings),
|
|
1907
|
+
model,
|
|
1908
|
+
authFallbackUsed,
|
|
1909
|
+
});
|
|
1910
|
+
if (retryFallbackRole) {
|
|
1911
|
+
logger.debug("Configured subagent runtime model fallback chain", {
|
|
1912
|
+
role: retryFallbackRole,
|
|
1913
|
+
requested: modelPatterns,
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1820
1916
|
if (model?.contextWindow && model.contextWindow > 0) {
|
|
1821
1917
|
progress.contextWindow = model.contextWindow;
|
|
1822
1918
|
}
|
|
1823
1919
|
if (model) {
|
|
1824
1920
|
progress.resolvedModel = explicitThinkingLevel
|
|
1825
|
-
?
|
|
1826
|
-
:
|
|
1921
|
+
? formatModelSelectorValue(formatModelStringWithRouting(model), resolvedThinkingLevel)
|
|
1922
|
+
: formatModelStringWithRouting(model);
|
|
1827
1923
|
}
|
|
1828
1924
|
const effectiveThinkingLevel = explicitThinkingLevel
|
|
1829
1925
|
? resolvedThinkingLevel
|
|
1830
1926
|
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
1831
1927
|
|
|
1928
|
+
const effectiveCwd = worktree ?? cwd;
|
|
1832
1929
|
const sessionManager = sessionFile
|
|
1833
|
-
? await awaitAbortable(SessionManager.open(sessionFile))
|
|
1834
|
-
: SessionManager.inMemory(
|
|
1930
|
+
? await awaitAbortable(SessionManager.open(sessionFile, undefined, undefined, { initialCwd: effectiveCwd }))
|
|
1931
|
+
: SessionManager.inMemory(effectiveCwd);
|
|
1835
1932
|
if (options.parentArtifactManager) {
|
|
1836
1933
|
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
1837
1934
|
}
|
|
@@ -2047,7 +2144,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2047
2144
|
{
|
|
2048
2145
|
getModel: () => session.model,
|
|
2049
2146
|
isIdle: () => !session.isStreaming,
|
|
2050
|
-
abort: () => session.abort(),
|
|
2147
|
+
abort: () => session.abort({ reason: USER_INTERRUPT_LABEL }),
|
|
2051
2148
|
hasPendingMessages: () => session.queuedMessageCount > 0,
|
|
2052
2149
|
shutdown: () => {},
|
|
2053
2150
|
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
|
|
|
@@ -66,6 +66,7 @@ const EXTENSION_LANG: Record<string, readonly [string, string]> = {
|
|
|
66
66
|
cljc: ["clojure", "clojure"],
|
|
67
67
|
cljs: ["clojure", "clojure"],
|
|
68
68
|
edn: ["clojure", "clojure"],
|
|
69
|
+
el: ["emacs-lisp", "emacs-lisp"],
|
|
69
70
|
|
|
70
71
|
// .NET
|
|
71
72
|
cs: ["csharp", "csharp"],
|
|
@@ -209,6 +210,7 @@ export function getLanguageFromPath(filePath: string): string | undefined {
|
|
|
209
210
|
if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
|
|
210
211
|
return "dockerfile";
|
|
211
212
|
}
|
|
213
|
+
if (baseName === ".emacs") return "emacs-lisp";
|
|
212
214
|
if (baseName === "justfile") return "just";
|
|
213
215
|
if (baseName === "cmakelists.txt") return "cmake";
|
|
214
216
|
|
|
@@ -223,6 +225,9 @@ export function detectLanguageId(filePath: string): string {
|
|
|
223
225
|
if (baseName === "dockerfile" || baseName.startsWith("dockerfile.") || baseName === "containerfile") {
|
|
224
226
|
return "dockerfile";
|
|
225
227
|
}
|
|
228
|
+
if (baseName === ".emacs") {
|
|
229
|
+
return "emacs-lisp";
|
|
230
|
+
}
|
|
226
231
|
if (baseName === "makefile" || baseName === "gnumakefile") {
|
|
227
232
|
return "makefile";
|
|
228
233
|
}
|
package/src/utils/markit.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { Markit, StreamInfo } from "markit-ai";
|
|
3
3
|
import { ToolAbortError } from "../tools/tool-errors";
|
|
4
4
|
|
|
@@ -8,6 +8,29 @@ export interface MarkitConversionResult {
|
|
|
8
8
|
error?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
interface MuPdfWasmModuleConfig {
|
|
12
|
+
print?: (...values: unknown[]) => void;
|
|
13
|
+
printErr?: (...values: unknown[]) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
declare global {
|
|
17
|
+
var $libmupdf_wasm_Module: MuPdfWasmModuleConfig | undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function logMuPdfWasmOutput(stream: "stdout" | "stderr", values: unknown[]): void {
|
|
21
|
+
const message = values.length === 1 && typeof values[0] === "string" ? values[0] : values.map(String).join(" ");
|
|
22
|
+
logger.debug("mupdf wasm output", { stream, message });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function installMuPdfWasmLogger(): void {
|
|
26
|
+
const moduleConfig = globalThis.$libmupdf_wasm_Module ?? {};
|
|
27
|
+
moduleConfig.print = (...values) => logMuPdfWasmOutput("stdout", values);
|
|
28
|
+
moduleConfig.printErr = (...values) => logMuPdfWasmOutput("stderr", values);
|
|
29
|
+
globalThis.$libmupdf_wasm_Module = moduleConfig;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
installMuPdfWasmLogger();
|
|
33
|
+
|
|
11
34
|
let markit: () => Markit | Promise<Markit> = async () => {
|
|
12
35
|
const promise = import("markit-ai").then(({ Markit }) => {
|
|
13
36
|
const instance = new Markit();
|
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);
|