@oh-my-pi/pi-coding-agent 15.9.1 → 15.9.5
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 +68 -2
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/cli/dry-balance-cli.d.ts +104 -0
- package/dist/types/commands/dry-balance.d.ts +31 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +13 -4
- package/dist/types/config/settings.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
- package/dist/types/hindsight/bank.d.ts +17 -9
- package/dist/types/hindsight/mental-models.d.ts +1 -1
- package/dist/types/hindsight/state.d.ts +9 -3
- package/dist/types/mcp/manager.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/error-banner.d.ts +11 -0
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -2
- package/dist/types/modes/components/user-message.d.ts +1 -1
- package/dist/types/modes/image-references.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/auth-storage.d.ts +2 -2
- package/dist/types/session/blob-store.d.ts +12 -11
- package/dist/types/session/session-manager.d.ts +5 -3
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +16 -1
- package/dist/types/tool-discovery/mode.d.ts +8 -0
- package/dist/types/tools/archive-reader.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +11 -0
- package/dist/types/tui/hyperlink.d.ts +12 -0
- package/dist/types/web/search/render.d.ts +1 -2
- package/package.json +9 -9
- package/src/cli/classify-install-target.ts +31 -5
- package/src/cli/dry-balance-cli.ts +823 -0
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/cli-commands.ts +1 -0
- package/src/commands/dry-balance.ts +43 -0
- package/src/config/model-registry.ts +60 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +14 -4
- package/src/config/settings.ts +38 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
- package/src/discovery/github.ts +37 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/eval/__tests__/agent-bridge.test.ts +72 -0
- package/src/eval/py/tool-bridge.ts +43 -5
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
- package/src/hindsight/backend.ts +184 -35
- package/src/hindsight/bank.ts +32 -22
- package/src/hindsight/mental-models.ts +1 -1
- package/src/hindsight/state.ts +21 -7
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/omp-protocol.ts +8 -2
- package/src/main.ts +7 -1
- package/src/mcp/manager.ts +40 -21
- package/src/modes/components/assistant-message.ts +22 -0
- package/src/modes/components/custom-editor.ts +14 -2
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/tool-execution.ts +44 -0
- package/src/modes/components/transcript-container.ts +102 -30
- package/src/modes/components/tree-selector.ts +29 -2
- package/src/modes/components/user-message.ts +9 -2
- package/src/modes/controllers/event-controller.ts +42 -3
- package/src/modes/controllers/input-controller.ts +41 -3
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +48 -13
- package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
- package/src/modes/types.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/librarian.md +1 -0
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/sdk.ts +99 -18
- package/src/session/agent-session.ts +103 -19
- package/src/session/auth-storage.ts +4 -0
- package/src/session/blob-store.ts +96 -9
- package/src/session/session-manager.ts +19 -10
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +6 -2
- package/src/task/index.ts +8 -7
- package/src/task/types.ts +2 -0
- package/src/tiny/title-client.ts +7 -1
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tools/archive-reader.ts +339 -31
- package/src/tools/bash.ts +3 -4
- package/src/tools/fetch.ts +29 -9
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +22 -8
- package/src/tools/job.ts +3 -3
- package/src/tools/memory-reflect.ts +2 -2
- package/src/tools/path-utils.ts +21 -0
- package/src/tools/read.ts +58 -12
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +78 -12
- package/src/tui/hyperlink.ts +42 -7
- package/src/utils/file-mentions.ts +7 -107
- package/src/utils/title-generator.ts +58 -37
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +20 -52
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -354,6 +354,7 @@ async function handleInstall(
|
|
|
354
354
|
console.error(chalk.dim(` ${APP_NAME} plugin install name@marketplace`));
|
|
355
355
|
console.error(chalk.dim(` ${APP_NAME} plugin install github:user/repo`));
|
|
356
356
|
console.error(chalk.dim(` ${APP_NAME} plugin install https://github.com/user/repo#v1.0`));
|
|
357
|
+
console.error(chalk.dim(` ${APP_NAME} plugin install ./path/to/local/plugin`));
|
|
357
358
|
process.exit(1);
|
|
358
359
|
}
|
|
359
360
|
|
|
@@ -382,6 +383,49 @@ async function handleInstall(
|
|
|
382
383
|
continue;
|
|
383
384
|
}
|
|
384
385
|
|
|
386
|
+
if (target.type === "local") {
|
|
387
|
+
// Local paths route to link(): symlink the directory into the plugins
|
|
388
|
+
// node_modules tree so source edits show up without a reinstall. Matches
|
|
389
|
+
// `omp plugin link <path>` so users can use either verb interchangeably.
|
|
390
|
+
if (flags.scope) {
|
|
391
|
+
console.error(
|
|
392
|
+
chalk.yellow(
|
|
393
|
+
`Warning: --scope is only supported for marketplace installs (name@marketplace). Ignoring for ${spec}.`,
|
|
394
|
+
),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (flags.force) {
|
|
398
|
+
console.error(
|
|
399
|
+
chalk.yellow(
|
|
400
|
+
`Warning: --force has no effect for local path installs (link is already idempotent). Ignoring for ${spec}.`,
|
|
401
|
+
),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (flags.dryRun) {
|
|
405
|
+
if (flags.json) {
|
|
406
|
+
console.log(JSON.stringify({ dryRun: true, action: "link", path: target.path }, null, 2));
|
|
407
|
+
} else {
|
|
408
|
+
console.log(chalk.dim(`[dry-run] Would link ${spec}`));
|
|
409
|
+
}
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
try {
|
|
413
|
+
const result = await manager.link(target.path);
|
|
414
|
+
if (flags.json) {
|
|
415
|
+
console.log(JSON.stringify(result, null, 2));
|
|
416
|
+
} else {
|
|
417
|
+
console.log(chalk.green(`${theme.status.success} Linked ${result.name} from ${spec}`));
|
|
418
|
+
if (result.manifest.description) {
|
|
419
|
+
console.log(chalk.dim(` ${result.manifest.description}`));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
} catch (err) {
|
|
423
|
+
console.error(chalk.red(`${theme.status.error} Failed to install ${spec}: ${err}`));
|
|
424
|
+
process.exit(1);
|
|
425
|
+
}
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
385
429
|
// --scope only applies to marketplace installs; warn when it would be silently no-op'd for npm.
|
|
386
430
|
if (flags.scope) {
|
|
387
431
|
console.error(
|
|
@@ -923,6 +967,7 @@ ${chalk.bold("Sources:")}
|
|
|
923
967
|
github:user/repo[#ref] GitHub shorthand (also gitlab:, bitbucket:, codeberg:, sourcehut:)
|
|
924
968
|
https://github.com/user/repo Full git URL (https, ssh, or git protocol)
|
|
925
969
|
name@marketplace Marketplace plugin (see marketplace command)
|
|
970
|
+
./path, ../path, /abs, ~/path Local plugin directory (symlinked, same as plugin link)
|
|
926
971
|
|
|
927
972
|
${chalk.bold("Config Subcommands:")}
|
|
928
973
|
config list <pkg> List all settings
|
|
@@ -97,7 +97,6 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
|
|
|
97
97
|
const result = await runSearchQuery(params);
|
|
98
98
|
const component = renderSearchResult(result, { expanded: cmd.expanded, isPartial: false }, theme, {
|
|
99
99
|
query: cmd.query,
|
|
100
|
-
allowLongAnswer: true,
|
|
101
100
|
maxAnswerLines: cmd.expanded ? undefined : 6,
|
|
102
101
|
});
|
|
103
102
|
|
package/src/cli-commands.ts
CHANGED
|
@@ -20,6 +20,7 @@ export const commands: CommandEntry[] = [
|
|
|
20
20
|
{ name: "completions", load: () => import("./commands/completions").then(m => m.default) },
|
|
21
21
|
{ name: "__complete", load: () => import("./commands/complete").then(m => m.default) },
|
|
22
22
|
{ name: "config", load: () => import("./commands/config").then(m => m.default) },
|
|
23
|
+
{ name: "dry-balance", load: () => import("./commands/dry-balance").then(m => m.default) },
|
|
23
24
|
{ name: "grep", load: () => import("./commands/grep").then(m => m.default) },
|
|
24
25
|
{ name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
|
|
25
26
|
{ name: "install", load: () => import("./commands/install").then(m => m.default) },
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
2
|
+
import { runDryBalanceCommand } from "../cli/dry-balance-cli";
|
|
3
|
+
|
|
4
|
+
export default class DryBalance extends Command {
|
|
5
|
+
static description = "Dry-run OAuth account balancing across random session ids";
|
|
6
|
+
|
|
7
|
+
static args = {
|
|
8
|
+
model: Args.string({
|
|
9
|
+
description: "Model selector (provider/model or fuzzy id). Defaults to the configured default model.",
|
|
10
|
+
required: false,
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
static flags = {
|
|
15
|
+
model: Flags.string({ description: "Model selector (same syntax as --model on omp)" }),
|
|
16
|
+
count: Flags.integer({ description: "Number of random session ids to try", default: 100 }),
|
|
17
|
+
concurrency: Flags.integer({ description: "Maximum concurrent credential resolutions", default: 32 }),
|
|
18
|
+
json: Flags.boolean({ description: "Output JSON" }),
|
|
19
|
+
bench: Flags.boolean({ description: "Send one live benchmark request per OAuth account" }),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
static examples = [
|
|
23
|
+
"# Dry-run the configured default model with 100 random session ids\n omp dry-balance",
|
|
24
|
+
"# Dry-run a specific model\n omp dry-balance anthropic/claude-sonnet-4-5",
|
|
25
|
+
"# Larger run with bounded concurrency\n omp dry-balance --model openai-codex/gpt-5-codex --count 1000 --concurrency 64",
|
|
26
|
+
"# Benchmark every OAuth account in parallel\n omp dry-balance --bench",
|
|
27
|
+
"# Machine-readable output\n omp dry-balance --json",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
async run(): Promise<void> {
|
|
31
|
+
const { args, flags } = await this.parse(DryBalance);
|
|
32
|
+
await runDryBalanceCommand({
|
|
33
|
+
model: args.model,
|
|
34
|
+
flags: {
|
|
35
|
+
model: flags.model,
|
|
36
|
+
count: flags.count,
|
|
37
|
+
concurrency: flags.concurrency,
|
|
38
|
+
json: flags.json,
|
|
39
|
+
bench: flags.bench,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -38,6 +38,45 @@ const DEFAULT_LOCAL_TOKEN = "lm-studio-local";
|
|
|
38
38
|
// "socket connection was closed unexpectedly").
|
|
39
39
|
const DISCOVERY_DEFAULT_MAX_TOKENS = 32_768;
|
|
40
40
|
|
|
41
|
+
const DEFAULT_OLLAMA_BASE_URL = "http://127.0.0.1:11434";
|
|
42
|
+
const OLLAMA_HOST_DEFAULT_PORT = "11434";
|
|
43
|
+
|
|
44
|
+
function normalizeOllamaHostEnv(value: string | undefined): string | undefined {
|
|
45
|
+
const trimmed = value?.trim();
|
|
46
|
+
if (!trimmed) return undefined;
|
|
47
|
+
const candidate = trimmed.includes("://")
|
|
48
|
+
? trimmed
|
|
49
|
+
: trimmed.startsWith("//")
|
|
50
|
+
? `http:${trimmed}`
|
|
51
|
+
: trimmed.startsWith(":")
|
|
52
|
+
? `http://127.0.0.1${trimmed}`
|
|
53
|
+
: `http://${trimmed}`;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = new URL(candidate);
|
|
56
|
+
if (!parsed.hostname || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
if (!parsed.port && parsed.protocol === "http:") {
|
|
60
|
+
parsed.port = OLLAMA_HOST_DEFAULT_PORT;
|
|
61
|
+
}
|
|
62
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
63
|
+
} catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getImplicitOllamaBaseUrl(): string {
|
|
69
|
+
const baseUrl = Bun.env.OLLAMA_BASE_URL?.trim();
|
|
70
|
+
return baseUrl || normalizeOllamaHostEnv(Bun.env.OLLAMA_HOST) || DEFAULT_OLLAMA_BASE_URL;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getOllamaContextLengthOverride(): number | undefined {
|
|
74
|
+
const value = Bun.env.OLLAMA_CONTEXT_LENGTH?.trim();
|
|
75
|
+
if (!value) return undefined;
|
|
76
|
+
const parsed = Number(value);
|
|
77
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
41
80
|
// Anthropic-safe variant of the discovery cap. The Anthropic stream converter
|
|
42
81
|
// in `packages/ai/src/providers/anthropic.ts` derives the request limit as
|
|
43
82
|
// `(model.maxTokens / 3) | 0`, so the 32K default would surface as 10,922
|
|
@@ -547,6 +586,7 @@ function applyModelOverride(model: Model<Api>, override: ModelOverride): Model<A
|
|
|
547
586
|
if (override.input !== undefined) result.input = override.input as ("text" | "image")[];
|
|
548
587
|
if (override.contextWindow !== undefined) result.contextWindow = override.contextWindow;
|
|
549
588
|
if (override.maxTokens !== undefined) result.maxTokens = override.maxTokens;
|
|
589
|
+
if (override.omitMaxOutputTokens !== undefined) result.omitMaxOutputTokens = override.omitMaxOutputTokens;
|
|
550
590
|
if (override.contextPromotionTarget !== undefined) result.contextPromotionTarget = override.contextPromotionTarget;
|
|
551
591
|
if (override.premiumMultiplier !== undefined) result.premiumMultiplier = override.premiumMultiplier;
|
|
552
592
|
if (override.cost) {
|
|
@@ -575,6 +615,7 @@ interface CustomModelDefinitionLike {
|
|
|
575
615
|
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
576
616
|
contextWindow?: number;
|
|
577
617
|
maxTokens?: number;
|
|
618
|
+
omitMaxOutputTokens?: boolean;
|
|
578
619
|
headers?: Record<string, string>;
|
|
579
620
|
compat?: Model<Api>["compat"];
|
|
580
621
|
contextPromotionTarget?: string;
|
|
@@ -597,6 +638,7 @@ type CustomModelOverlay = {
|
|
|
597
638
|
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
598
639
|
contextWindow?: number;
|
|
599
640
|
maxTokens?: number;
|
|
641
|
+
omitMaxOutputTokens?: boolean;
|
|
600
642
|
headers?: Record<string, string>;
|
|
601
643
|
compat?: Model<Api>["compat"];
|
|
602
644
|
contextPromotionTarget?: string;
|
|
@@ -667,6 +709,7 @@ function buildCustomModelOverlay(
|
|
|
667
709
|
cost: modelDef.cost,
|
|
668
710
|
contextWindow: modelDef.contextWindow,
|
|
669
711
|
maxTokens: modelDef.maxTokens,
|
|
712
|
+
omitMaxOutputTokens: modelDef.omitMaxOutputTokens,
|
|
670
713
|
headers: mergeCustomModelHeaders(providerHeaders, modelDef.headers, authHeader, providerApiKey),
|
|
671
714
|
compat: mergeCompat(providerCompat, modelDef.compat),
|
|
672
715
|
contextPromotionTarget: modelDef.contextPromotionTarget,
|
|
@@ -823,6 +866,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
823
866
|
resolvedModel.contextWindow ?? reference?.contextWindow ?? (options.useDefaults ? 128000 : undefined),
|
|
824
867
|
maxTokens: resolvedModel.maxTokens ?? reference?.maxTokens ?? (options.useDefaults ? 16384 : undefined),
|
|
825
868
|
headers: resolvedModel.headers,
|
|
869
|
+
omitMaxOutputTokens: resolvedModel.omitMaxOutputTokens ?? reference?.omitMaxOutputTokens,
|
|
826
870
|
compat: mergeCompat(reference?.compat, resolvedModel.compat),
|
|
827
871
|
contextPromotionTarget: resolvedModel.contextPromotionTarget,
|
|
828
872
|
premiumMultiplier: resolvedModel.premiumMultiplier,
|
|
@@ -1124,6 +1168,7 @@ export class ModelRegistry {
|
|
|
1124
1168
|
cost: customModel.cost ?? existingModel.cost,
|
|
1125
1169
|
contextWindow: customModel.contextWindow ?? existingModel.contextWindow,
|
|
1126
1170
|
maxTokens: customModel.maxTokens ?? existingModel.maxTokens,
|
|
1171
|
+
omitMaxOutputTokens: customModel.omitMaxOutputTokens ?? existingModel.omitMaxOutputTokens,
|
|
1127
1172
|
// Same-id custom definitions replace bundled transport behavior. Provider-level
|
|
1128
1173
|
// headers/compat were already folded into customModel during parsing; do not
|
|
1129
1174
|
// re-merge bundled transport metadata here.
|
|
@@ -1214,7 +1259,18 @@ export class ModelRegistry {
|
|
|
1214
1259
|
return models;
|
|
1215
1260
|
}
|
|
1216
1261
|
|
|
1217
|
-
|
|
1262
|
+
const contextLengthOverride = getOllamaContextLengthOverride();
|
|
1263
|
+
return models.map(model => {
|
|
1264
|
+
const normalized = model.api === "openai-completions" ? { ...model, api: "openai-responses" as const } : model;
|
|
1265
|
+
if (contextLengthOverride === undefined) {
|
|
1266
|
+
return normalized;
|
|
1267
|
+
}
|
|
1268
|
+
return {
|
|
1269
|
+
...normalized,
|
|
1270
|
+
contextWindow: contextLengthOverride,
|
|
1271
|
+
maxTokens: Math.min(contextLengthOverride, DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
1272
|
+
};
|
|
1273
|
+
});
|
|
1218
1274
|
}
|
|
1219
1275
|
|
|
1220
1276
|
#addImplicitDiscoverableProviders(configuredProviders: Set<string>): void {
|
|
@@ -1223,7 +1279,7 @@ export class ModelRegistry {
|
|
|
1223
1279
|
this.#discoverableProviders.push({
|
|
1224
1280
|
provider: "ollama",
|
|
1225
1281
|
api: "openai-responses",
|
|
1226
|
-
baseUrl:
|
|
1282
|
+
baseUrl: getImplicitOllamaBaseUrl(),
|
|
1227
1283
|
discovery: { type: "ollama" },
|
|
1228
1284
|
optional: true,
|
|
1229
1285
|
});
|
|
@@ -1987,12 +2043,12 @@ export class ModelRegistry {
|
|
|
1987
2043
|
}
|
|
1988
2044
|
}
|
|
1989
2045
|
#normalizeOllamaBaseUrl(baseUrl?: string): string {
|
|
1990
|
-
const raw = baseUrl ||
|
|
2046
|
+
const raw = baseUrl || DEFAULT_OLLAMA_BASE_URL;
|
|
1991
2047
|
try {
|
|
1992
2048
|
const parsed = new URL(raw);
|
|
1993
2049
|
return `${parsed.protocol}//${parsed.host}`;
|
|
1994
2050
|
} catch {
|
|
1995
|
-
return
|
|
2051
|
+
return DEFAULT_OLLAMA_BASE_URL;
|
|
1996
2052
|
}
|
|
1997
2053
|
}
|
|
1998
2054
|
|
|
@@ -93,6 +93,7 @@ const ModelDefinitionSchema = z.object({
|
|
|
93
93
|
premiumMultiplier: z.number().optional(),
|
|
94
94
|
contextWindow: z.number().optional(),
|
|
95
95
|
maxTokens: z.number().optional(),
|
|
96
|
+
omitMaxOutputTokens: z.boolean().optional(),
|
|
96
97
|
headers: z.record(z.string(), z.string()).optional(),
|
|
97
98
|
compat: OpenAICompatSchema.optional(),
|
|
98
99
|
contextPromotionTarget: z.string().min(1).optional(),
|
|
@@ -114,6 +115,7 @@ export const ModelOverrideSchema = z.object({
|
|
|
114
115
|
premiumMultiplier: z.number().optional(),
|
|
115
116
|
contextWindow: z.number().optional(),
|
|
116
117
|
maxTokens: z.number().optional(),
|
|
118
|
+
omitMaxOutputTokens: z.boolean().optional(),
|
|
117
119
|
headers: z.record(z.string(), z.string()).optional(),
|
|
118
120
|
compat: OpenAICompatSchema.optional(),
|
|
119
121
|
contextPromotionTarget: z.string().min(1).optional(),
|
|
@@ -635,7 +635,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
635
635
|
tab: "appearance",
|
|
636
636
|
label: "Terminal Hyperlinks",
|
|
637
637
|
description:
|
|
638
|
-
"Wrap
|
|
638
|
+
"Wrap paths and URLs in OSC 8 hyperlinks for terminal-native click-to-open (auto: detect support; off: never; always: unconditional)",
|
|
639
639
|
},
|
|
640
640
|
},
|
|
641
641
|
// Display rendering
|
|
@@ -722,6 +722,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
722
722
|
},
|
|
723
723
|
},
|
|
724
724
|
|
|
725
|
+
includeModelInPrompt: {
|
|
726
|
+
type: "boolean",
|
|
727
|
+
default: true,
|
|
728
|
+
ui: {
|
|
729
|
+
tab: "model",
|
|
730
|
+
label: "Include Model In Prompt",
|
|
731
|
+
description: "Surface the active model identifier in the system prompt so the agent knows which model it is",
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
|
|
725
735
|
// Sampling
|
|
726
736
|
temperature: {
|
|
727
737
|
type: "number",
|
|
@@ -2483,13 +2493,13 @@ export const SETTINGS_SCHEMA = {
|
|
|
2483
2493
|
// Tool Discovery
|
|
2484
2494
|
"tools.discoveryMode": {
|
|
2485
2495
|
type: "enum",
|
|
2486
|
-
values: ["off", "mcp-only", "all"] as const,
|
|
2487
|
-
default: "
|
|
2496
|
+
values: ["auto", "off", "mcp-only", "all"] as const,
|
|
2497
|
+
default: "auto",
|
|
2488
2498
|
ui: {
|
|
2489
2499
|
tab: "tools",
|
|
2490
2500
|
label: "Tool Discovery",
|
|
2491
2501
|
description:
|
|
2492
|
-
"Hide tools behind a search tool to save tokens. 'mcp-only' hides MCP tools; 'all' hides all non-essential built-ins too.",
|
|
2502
|
+
"Hide tools behind a search tool to save tokens. 'auto' hides MCP tools once the tool set has more than 40 tools; 'mcp-only' always hides MCP tools; 'all' hides all non-essential built-ins too.",
|
|
2493
2503
|
},
|
|
2494
2504
|
},
|
|
2495
2505
|
|
package/src/config/settings.ts
CHANGED
|
@@ -907,6 +907,9 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
|
|
|
907
907
|
for (const cb of appendOnlyModeCallbacks) cb(value);
|
|
908
908
|
}
|
|
909
909
|
},
|
|
910
|
+
"hindsight.bankId": () => fireHindsightScopeChanged(),
|
|
911
|
+
"hindsight.bankIdPrefix": () => fireHindsightScopeChanged(),
|
|
912
|
+
"hindsight.scoping": () => fireHindsightScopeChanged(),
|
|
910
913
|
};
|
|
911
914
|
/** Callbacks invoked when `provider.appendOnlyContext` changes at runtime. */
|
|
912
915
|
const appendOnlyModeCallbacks = new Set<(value: string) => void>();
|
|
@@ -923,6 +926,41 @@ export function onAppendOnlyModeChanged(cb: (value: string) => void): () => void
|
|
|
923
926
|
};
|
|
924
927
|
}
|
|
925
928
|
|
|
929
|
+
/** Callbacks fired when any `hindsight.bankId` / `bankIdPrefix` / `scoping` value changes. */
|
|
930
|
+
const hindsightScopeCallbacks = new Set<() => void>();
|
|
931
|
+
|
|
932
|
+
function fireHindsightScopeChanged(): void {
|
|
933
|
+
// Snapshot the callback set before invoking — a callback's body is allowed
|
|
934
|
+
// to subscribe a NEW callback (the Hindsight backend re-registers the
|
|
935
|
+
// fresh state's listener on every rebuild). Iterating the live Set would
|
|
936
|
+
// re-invoke those just-added callbacks within the same fire, which spins
|
|
937
|
+
// in place: subscribe → invoke → subscribe → invoke → …
|
|
938
|
+
for (const cb of [...hindsightScopeCallbacks]) {
|
|
939
|
+
try {
|
|
940
|
+
cb();
|
|
941
|
+
} catch (err) {
|
|
942
|
+
logger.warn("Settings: hindsight scope hook failed", { error: String(err) });
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Subscribe to changes in the Hindsight bank-scoping settings. Lets the
|
|
949
|
+
* Hindsight backend rebuild the active `HindsightSessionState` when the
|
|
950
|
+
* operator switches `hindsight.bankId`, `hindsight.bankIdPrefix`, or
|
|
951
|
+
* `hindsight.scoping` mid-session so subsequent retain/recall calls land in
|
|
952
|
+
* the new bank instead of the one selected at session start.
|
|
953
|
+
*
|
|
954
|
+
* Returns an unsubscribe function. The callback receives no arguments — the
|
|
955
|
+
* caller is expected to re-read the relevant settings via `Settings.get`.
|
|
956
|
+
*/
|
|
957
|
+
export function onHindsightScopeChanged(cb: () => void): () => void {
|
|
958
|
+
hindsightScopeCallbacks.add(cb);
|
|
959
|
+
return () => {
|
|
960
|
+
hindsightScopeCallbacks.delete(cb);
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
|
|
926
964
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
927
965
|
// Global Singleton
|
|
928
966
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -46,5 +46,6 @@ const doubled = value * 2;
|
|
|
46
46
|
- Callback identity matters.
|
|
47
47
|
- Type guard preserves narrowing.
|
|
48
48
|
- Public API, test seam, or DI boundary needs indirection.
|
|
49
|
+
- Names a non-obvious formula or magic-constant computation that the inlined expression would not explain on its own.
|
|
49
50
|
|
|
50
51
|
If none apply, inline it.
|
package/src/discovery/github.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Capabilities:
|
|
11
11
|
* - context-files: copilot-instructions.md in .github/
|
|
12
12
|
* - instructions: *.instructions.md in .github/instructions/ with applyTo frontmatter
|
|
13
|
+
* - skills: <name>/SKILL.md in .github/skills/ (GitHub Agent Skills layout)
|
|
13
14
|
*/
|
|
14
15
|
import * as path from "node:path";
|
|
15
16
|
import { parseFrontmatter } from "@oh-my-pi/pi-utils";
|
|
@@ -17,9 +18,10 @@ import { registerProvider } from "../capability";
|
|
|
17
18
|
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
18
19
|
import { readFile } from "../capability/fs";
|
|
19
20
|
import { type Instruction, instructionCapability } from "../capability/instruction";
|
|
21
|
+
import { type Skill, skillCapability } from "../capability/skill";
|
|
20
22
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
21
23
|
|
|
22
|
-
import { calculateDepth, createSourceMeta, getProjectPath, loadFilesFromDir } from "./helpers";
|
|
24
|
+
import { calculateDepth, createSourceMeta, getProjectPath, loadFilesFromDir, scanSkillsFromDir } from "./helpers";
|
|
23
25
|
|
|
24
26
|
const PROVIDER_ID = "github";
|
|
25
27
|
const DISPLAY_NAME = "GitHub Copilot";
|
|
@@ -97,6 +99,32 @@ function transformInstruction(name: string, content: string, filePath: string, s
|
|
|
97
99
|
};
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
// =============================================================================
|
|
103
|
+
// Skills
|
|
104
|
+
// =============================================================================
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Load skills from `.github/skills/<name>/SKILL.md`.
|
|
108
|
+
*
|
|
109
|
+
* GitHub documents this layout for Copilot Agent Skills and matches the
|
|
110
|
+
* non-recursive shape `scanSkillsFromDir` already expects. `requireDescription`
|
|
111
|
+
* is on to match the Agent Skills spec (name + description are mandatory) and
|
|
112
|
+
* the sibling `native`/`omp-plugins` providers.
|
|
113
|
+
*
|
|
114
|
+
* @see https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/add-skills
|
|
115
|
+
*/
|
|
116
|
+
async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
|
|
117
|
+
const skillsDir = getProjectPath(ctx, "github", "skills");
|
|
118
|
+
if (!skillsDir) return { items: [], warnings: [] };
|
|
119
|
+
|
|
120
|
+
return scanSkillsFromDir(ctx, {
|
|
121
|
+
dir: skillsDir,
|
|
122
|
+
providerId: PROVIDER_ID,
|
|
123
|
+
level: "project",
|
|
124
|
+
requireDescription: true,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
100
128
|
// =============================================================================
|
|
101
129
|
// Provider Registration
|
|
102
130
|
// =============================================================================
|
|
@@ -116,3 +144,11 @@ registerProvider(instructionCapability.id, {
|
|
|
116
144
|
priority: PRIORITY,
|
|
117
145
|
load: loadInstructions,
|
|
118
146
|
});
|
|
147
|
+
|
|
148
|
+
registerProvider<Skill>(skillCapability.id, {
|
|
149
|
+
id: PROVIDER_ID,
|
|
150
|
+
displayName: DISPLAY_NAME,
|
|
151
|
+
description: "Load skills from .github/skills/*/SKILL.md",
|
|
152
|
+
priority: PRIORITY,
|
|
153
|
+
load: loadSkills,
|
|
154
|
+
});
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -212,6 +212,7 @@ export interface ParsedAgentFields {
|
|
|
212
212
|
output?: unknown;
|
|
213
213
|
thinkingLevel?: ThinkingLevel;
|
|
214
214
|
autoloadSkills?: string[];
|
|
215
|
+
readSummarize?: boolean;
|
|
215
216
|
blocking?: boolean;
|
|
216
217
|
}
|
|
217
218
|
|
|
@@ -265,10 +266,11 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
|
|
|
265
266
|
const thinkingLevel = parseThinkingLevel(rawThinkingLevel);
|
|
266
267
|
const model = parseModelList(frontmatter.model);
|
|
267
268
|
const blocking = parseBoolean(frontmatter.blocking);
|
|
269
|
+
const readSummarize = parseBoolean(frontmatter.readSummarize);
|
|
268
270
|
const autoloadSkills = parseArrayOrCSV(frontmatter.autoloadSkills)
|
|
269
271
|
?.map(s => s.trim())
|
|
270
272
|
.filter(Boolean);
|
|
271
|
-
return { name, description, tools, spawns, model, output, thinkingLevel, blocking, autoloadSkills };
|
|
273
|
+
return { name, description, tools, spawns, model, output, thinkingLevel, blocking, autoloadSkills, readSummarize };
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
async function globIf(
|
|
@@ -397,6 +397,78 @@ describe("agent() through eval runtimes", () => {
|
|
|
397
397
|
expect(maxInFlight).toBeLessThanOrEqual(2);
|
|
398
398
|
});
|
|
399
399
|
|
|
400
|
+
it("interrupting a Python parallel() fan-out settles the kernel cleanly and preserves session state", async () => {
|
|
401
|
+
using tempDir = TempDir.createSync("@omp-eval-agent-py-interrupt-");
|
|
402
|
+
const settings = Settings.isolated({
|
|
403
|
+
"async.enabled": false,
|
|
404
|
+
"task.isolation.mode": "none",
|
|
405
|
+
"task.enableLsp": true,
|
|
406
|
+
"task.maxConcurrency": 6,
|
|
407
|
+
});
|
|
408
|
+
const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "py-agent-interrupt", settings);
|
|
409
|
+
mockAgents();
|
|
410
|
+
// Subagents that ignore the abort for far longer than the kernel's SIGINT
|
|
411
|
+
// escalation window. Each kernel worker thread blocks in a synchronous
|
|
412
|
+
// `urllib` bridge call, joined by `parallel()`'s ThreadPoolExecutor exit.
|
|
413
|
+
// The host must respond the instant the cell aborts so the kernel can
|
|
414
|
+
// unwind via KeyboardInterrupt instead of being hard-killed (which used to
|
|
415
|
+
// surface "[kernel] Python kernel shutdown" and lose all session state).
|
|
416
|
+
vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
|
|
417
|
+
await Bun.sleep(9000); // deliberately ignores options.signal
|
|
418
|
+
return singleResult(options, { output: options.assignment ?? "" });
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// Seed persistent session state and confirm the kernel is reusable.
|
|
422
|
+
const seed = await executePython("PREP_MARKER = 4242", {
|
|
423
|
+
cwd: tempDir.path(),
|
|
424
|
+
sessionId,
|
|
425
|
+
sessionFile,
|
|
426
|
+
kernelMode: "session",
|
|
427
|
+
toolSession: session,
|
|
428
|
+
});
|
|
429
|
+
if (seed.exitCode === undefined && seed.cancelled) {
|
|
430
|
+
expect(seed.output).toBe("");
|
|
431
|
+
return; // kernel unavailable in this environment
|
|
432
|
+
}
|
|
433
|
+
expect(seed.exitCode).toBe(0);
|
|
434
|
+
|
|
435
|
+
const ac = new AbortController();
|
|
436
|
+
// Abort ~1s in, after the worker threads are blocked in their bridge calls.
|
|
437
|
+
setTimeout(() => ac.abort(new Error("external interrupt")), 1000);
|
|
438
|
+
|
|
439
|
+
const start = Date.now();
|
|
440
|
+
const result = await executePython(
|
|
441
|
+
"import json\nprint(json.dumps(parallel([lambda n=n: agent(str(n)) for n in range(12)])))",
|
|
442
|
+
{
|
|
443
|
+
cwd: tempDir.path(),
|
|
444
|
+
sessionId,
|
|
445
|
+
sessionFile,
|
|
446
|
+
kernelMode: "session",
|
|
447
|
+
toolSession: session,
|
|
448
|
+
idleTimeoutMs: 60_000,
|
|
449
|
+
signal: ac.signal,
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
const elapsed = Date.now() - start;
|
|
453
|
+
|
|
454
|
+
// Cancelled, but cleanly: no hard-kill, settled well within the kernel's 5s
|
|
455
|
+
// SIGINT escalation window rather than ~6s after it.
|
|
456
|
+
expect(result.cancelled).toBe(true);
|
|
457
|
+
expect(result.output).not.toContain("Python kernel shutdown");
|
|
458
|
+
expect(elapsed).toBeLessThan(4000);
|
|
459
|
+
|
|
460
|
+
// The persistent kernel survived the interrupt: prior state is intact.
|
|
461
|
+
const after = await executePython("print(PREP_MARKER)", {
|
|
462
|
+
cwd: tempDir.path(),
|
|
463
|
+
sessionId,
|
|
464
|
+
sessionFile,
|
|
465
|
+
kernelMode: "session",
|
|
466
|
+
toolSession: session,
|
|
467
|
+
});
|
|
468
|
+
expect(after.exitCode).toBe(0);
|
|
469
|
+
expect(after.output.trim()).toBe("4242");
|
|
470
|
+
}, 30_000);
|
|
471
|
+
|
|
400
472
|
it("streams enriched agent progress through onStatus before the cell finishes", async () => {
|
|
401
473
|
using tempDir = TempDir.createSync("@omp-eval-agent-progress-");
|
|
402
474
|
const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "js-agent-progress");
|
|
@@ -30,6 +30,48 @@ interface BridgeServer {
|
|
|
30
30
|
const registrations = new Map<string, PyToolBridgeEntry>();
|
|
31
31
|
let serverPromise: Promise<BridgeServer> | null = null;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Forward a bridge call to {@link callSessionTool}, but resolve the HTTP request
|
|
35
|
+
* the instant the cell's signal aborts instead of waiting for the tool/subagent
|
|
36
|
+
* to fully tear down.
|
|
37
|
+
*
|
|
38
|
+
* The kernel invokes this bridge with a *blocking* `urllib` request from a
|
|
39
|
+
* worker thread (each `agent()` / `tool.*` call). When the cell is interrupted,
|
|
40
|
+
* `parallel()`'s `ThreadPoolExecutor.__exit__` joins those worker threads
|
|
41
|
+
* (`shutdown(wait=True)`), so they cannot unwind until their `urllib` call
|
|
42
|
+
* returns — i.e. until this handler responds. A host-side `agent()` teardown
|
|
43
|
+
* (aborting nested LLM streams + tools across a wide fan-out) routinely exceeds
|
|
44
|
+
* the kernel's SIGINT escalation window, so the kernel was hard-killed and its
|
|
45
|
+
* persistent state lost while the subagents were still winding down. Responding
|
|
46
|
+
* immediately on abort lets the kernel raise through the blocked call and settle
|
|
47
|
+
* cleanly (preserving state); the already-signaled call keeps tearing down in
|
|
48
|
+
* the background, its eventual result/rejection swallowed.
|
|
49
|
+
*/
|
|
50
|
+
async function callSessionToolPromptOnAbort(name: string, args: unknown, entry: PyToolBridgeEntry): Promise<unknown> {
|
|
51
|
+
const call = callSessionTool(name, args, {
|
|
52
|
+
session: entry.toolSession,
|
|
53
|
+
signal: entry.signal,
|
|
54
|
+
emitStatus: entry.emitStatus,
|
|
55
|
+
});
|
|
56
|
+
const signal = entry.signal;
|
|
57
|
+
if (!signal) return await call;
|
|
58
|
+
if (signal.aborted) {
|
|
59
|
+
void call.catch(() => {});
|
|
60
|
+
throw new Error(`bridge call ${JSON.stringify(name)} aborted: eval cell was interrupted`);
|
|
61
|
+
}
|
|
62
|
+
const { promise: aborted, reject } = Promise.withResolvers<never>();
|
|
63
|
+
const onAbort = () => reject(new Error(`bridge call ${JSON.stringify(name)} aborted: eval cell was interrupted`));
|
|
64
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
65
|
+
try {
|
|
66
|
+
return await Promise.race([call, aborted]);
|
|
67
|
+
} finally {
|
|
68
|
+
signal.removeEventListener("abort", onAbort);
|
|
69
|
+
// `call` may still be settling (subagent teardown after its own abort);
|
|
70
|
+
// swallow its outcome so an abort-won race can't surface as unhandled.
|
|
71
|
+
void call.catch(() => {});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
33
75
|
async function startServer(): Promise<BridgeServer> {
|
|
34
76
|
const token = crypto.randomUUID();
|
|
35
77
|
const server = Bun.serve({
|
|
@@ -66,11 +108,7 @@ async function startServer(): Promise<BridgeServer> {
|
|
|
66
108
|
}
|
|
67
109
|
|
|
68
110
|
try {
|
|
69
|
-
const value = await
|
|
70
|
-
session: entry.toolSession,
|
|
71
|
-
signal: entry.signal,
|
|
72
|
-
emitStatus: entry.emitStatus,
|
|
73
|
-
});
|
|
111
|
+
const value = await callSessionToolPromptOnAbort(name, body.args, entry);
|
|
74
112
|
return Response.json({ ok: true, value });
|
|
75
113
|
} catch (err) {
|
|
76
114
|
return Response.json({
|
|
@@ -12,6 +12,35 @@ async function getHeadTag(api: CustomCommandAPI): Promise<string | undefined> {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
async function getCurrentBranch(api: CustomCommandAPI): Promise<string> {
|
|
16
|
+
try {
|
|
17
|
+
return (await git.branch.current(api.cwd)) ?? "HEAD";
|
|
18
|
+
} catch {
|
|
19
|
+
return "HEAD";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getPushRemote(api: CustomCommandAPI, branch: string): Promise<string | undefined> {
|
|
24
|
+
try {
|
|
25
|
+
return (
|
|
26
|
+
(await git.config.getBranch(api.cwd, branch, "pushRemote")) ??
|
|
27
|
+
(await git.config.getBranch(api.cwd, branch, "remote"))
|
|
28
|
+
);
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function getHeadTagContext(api: CustomCommandAPI): Promise<{ branch: string; headTag?: string; remote: string }> {
|
|
35
|
+
const branch = await getCurrentBranch(api);
|
|
36
|
+
const [headTag, pushRemote] = await Promise.all([getHeadTag(api), getPushRemote(api, branch)]);
|
|
37
|
+
return {
|
|
38
|
+
headTag,
|
|
39
|
+
branch,
|
|
40
|
+
remote: pushRemote ?? "origin",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
15
44
|
export class GreenCommand implements CustomCommand {
|
|
16
45
|
name = "green";
|
|
17
46
|
description = "Generate a prompt to iterate on CI failures until the branch is green";
|
|
@@ -19,7 +48,7 @@ export class GreenCommand implements CustomCommand {
|
|
|
19
48
|
constructor(private api: CustomCommandAPI) {}
|
|
20
49
|
|
|
21
50
|
async execute(_args: string[], _ctx: HookCommandContext): Promise<string> {
|
|
22
|
-
const headTag = await
|
|
23
|
-
return prompt.render(ciGreenRequestTemplate, { headTag });
|
|
51
|
+
const { headTag, branch, remote } = await getHeadTagContext(this.api);
|
|
52
|
+
return prompt.render(ciGreenRequestTemplate, { headTag, branch, remote });
|
|
24
53
|
}
|
|
25
54
|
}
|