@selesai/code 0.8.6 → 0.8.7
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 +16 -0
- package/dist/core/agent-session.js +13 -1
- package/dist/core/extensions/types.d.ts +2 -0
- package/dist/core/provider-composer.js +17 -4
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +3 -0
- package/dist/core/system-prompt.js +2 -8
- package/dist/core/system-prompt.test.js +6 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/schema-prune.d.ts +16 -0
- package/dist/core/tools/schema-prune.js +39 -0
- package/dist/core/tools/schema-prune.test.d.ts +1 -0
- package/dist/core/tools/schema-prune.test.js +111 -0
- package/dist/defaults/settings.json +1 -0
- package/dist/extensions/model-prompt-injector/config.json +10 -0
- package/dist/extensions/model-prompt-injector/index.test.ts +112 -0
- package/dist/extensions/model-prompt-injector/index.ts +151 -0
- package/dist/extensions/package.json +1 -0
- package/dist/extensions/pi-subagents/CHANGELOG.md +158 -90
- package/dist/extensions/pi-subagents/UPSTREAM-V0.50-MAPPING.md +172 -0
- package/dist/extensions/pi-subagents/agents/oracle.md +7 -5
- package/dist/extensions/pi-subagents/agents/reviewer.md +2 -2
- package/dist/extensions/pi-subagents/agents/scout.md +1 -1
- package/dist/extensions/pi-subagents/agents/worker.md +1 -1
- package/dist/extensions/pi-subagents/docs/configuration.md +78 -19
- package/dist/extensions/pi-subagents/docs/extension-api.md +36 -0
- package/dist/extensions/pi-subagents/docs/missions.md +5 -3
- package/dist/extensions/pi-subagents/docs/models.md +1 -1
- package/dist/extensions/pi-subagents/docs/observability.md +42 -2
- package/dist/extensions/pi-subagents/docs/tool-reference.md +20 -3
- package/dist/extensions/pi-subagents/docs/workflows.md +5 -5
- package/dist/extensions/pi-subagents/install.mjs +2 -2
- package/dist/extensions/pi-subagents/package-lock.json +1470 -1472
- package/dist/extensions/pi-subagents/package.json +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +2 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +37 -12
- package/dist/extensions/pi-subagents/src/api/external-runs.ts +174 -84
- package/dist/extensions/pi-subagents/src/api/preflight.ts +13 -7
- package/dist/extensions/pi-subagents/src/extension/config.ts +53 -0
- package/dist/extensions/pi-subagents/src/extension/index.ts +67 -13
- package/dist/extensions/pi-subagents/src/extension/public-execution.ts +34 -6
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +5 -1
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +31 -30
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +10 -9
- package/dist/extensions/pi-subagents/src/inspectors/herdr/actions.ts +13 -8
- package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/dist/extensions/pi-subagents/src/inspectors/herdr/project-panes.ts +2 -6
- package/dist/extensions/pi-subagents/src/inspectors/herdr/shell-command.ts +16 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +5 -4
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +19 -42
- package/dist/extensions/pi-subagents/src/missions/goal-driver.ts +3 -1
- package/dist/extensions/pi-subagents/src/missions/store.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +82 -25
- package/dist/extensions/pi-subagents/src/runs/background/active-run-index.ts +71 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +73 -43
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +5 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/background/async-status-snapshot.ts +277 -0
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +8 -3
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/background/completion-replay.ts +11 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +21 -6
- package/dist/extensions/pi-subagents/src/runs/background/result-files.ts +437 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +205 -41
- package/dist/extensions/pi-subagents/src/runs/background/resume-guidance.ts +27 -7
- package/dist/extensions/pi-subagents/src/runs/background/retained-children.ts +76 -19
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +30 -24
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +101 -4
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +54 -28
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +27 -13
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +298 -33
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +5 -2
- package/dist/extensions/pi-subagents/src/runs/foreground/async-dismiss-action.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +16 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +219 -15
- package/dist/extensions/pi-subagents/src/runs/foreground/prompt-audit.ts +4 -3
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +333 -45
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +107 -1
- package/dist/extensions/pi-subagents/src/runs/shared/external-cli-runner.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/llm-intent-arbiter.ts +39 -23
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +16 -2
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +66 -62
- package/dist/extensions/pi-subagents/src/runs/shared/orca-progress-tabs.ts +376 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +15 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +1 -9
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-startup-retry.ts +12 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +95 -0
- package/dist/extensions/pi-subagents/src/shared/agent-stream-options.ts +5 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +0 -4
- package/dist/extensions/pi-subagents/src/shared/display-text.ts +50 -0
- package/dist/extensions/pi-subagents/src/shared/node-executable.ts +21 -0
- package/dist/extensions/pi-subagents/src/shared/types.ts +50 -4
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +34 -25
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +4 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +160 -45
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +1 -48
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +128 -16
- package/dist/extensions/pi-subagents/src/tui/render.ts +122 -44
- package/dist/extensions/pi-subagents/src/watchdog/permission-arbiter.ts +2 -1
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +4 -3
- package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +10 -2
- package/dist/extensions/pi-subagents/src/workflows/scripted-workflow.ts +272 -76
- package/dist/extensions/pi-subagents/test/fixtures/pi-coding-agent-shim/dist/core/extensions/types.d.ts +2 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +68 -9
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +27 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +49 -0
- package/dist/extensions/pi-subagents/test/integration/external-cli-runner.test.ts +1 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +26 -1
- package/dist/extensions/pi-subagents/test/integration/orca-progress-tabs.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +369 -98
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +111 -78
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +49 -1
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +12 -0
- package/dist/extensions/pi-subagents/test/support/node-command.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +50 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +61 -0
- package/dist/extensions/pi-subagents/test/unit/agent-stream-options.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
- package/dist/extensions/pi-subagents/test/unit/async-status-snapshot.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +209 -10
- package/dist/extensions/pi-subagents/test/unit/completion-replay.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/external-runs.test.ts +135 -42
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +252 -1
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +192 -6
- package/dist/extensions/pi-subagents/test/unit/herdr-inspector-bootstrap.test.ts +32 -0
- package/dist/extensions/pi-subagents/test/unit/herdr-shell-command.test.ts +59 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +69 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +23 -4
- package/dist/extensions/pi-subagents/test/unit/llm-intent-arbiter.test.ts +56 -1
- package/dist/extensions/pi-subagents/test/unit/mission-goal-driver.test.ts +67 -3
- package/dist/extensions/pi-subagents/test/unit/mission-lifecycle.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/mission-store.test.ts +14 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +18 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +2 -2
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +25 -2
- package/dist/extensions/pi-subagents/test/unit/node-executable.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +8 -0
- package/dist/extensions/pi-subagents/test/unit/orca-progress-tabs.test.ts +348 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +5 -2
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/public-execution.test.ts +6 -1
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +92 -0
- package/dist/extensions/pi-subagents/test/unit/result-files.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/retained-children.test.ts +127 -17
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +44 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +107 -2
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +5 -8
- package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +456 -4
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +28 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +16 -7
- package/dist/extensions/pi-subagents/test/unit/subagent-startup-retry.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +79 -15
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +12 -12
- package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +109 -0
- package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +34 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +42 -2
- package/dist/extensions/question/index.ts +21 -6
- package/dist/extensions/question/question-list.ts +7 -1
- package/dist/extensions/question/row-layout.ts +3 -0
- package/dist/extensions/question/tests/exp.test.ts +70 -0
- package/dist/extensions/question/tests/exp2.test.ts +59 -0
- package/dist/extensions/question/tests/exp3.test.ts +47 -0
- package/dist/extensions/question/tests/helpers.test.ts +7 -0
- package/dist/extensions/question/tests/question-list.test.ts +140 -0
- package/dist/extensions/question/tests/row-layout.test.ts +13 -0
- package/dist/extensions/question/tests/selection-mode.test.ts +54 -0
- package/dist/extensions/question/tests/zz-ig3.test.ts +20 -0
- package/dist/extensions/question/tests/zz-probe.test.ts +29 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -1
- package/dist/skills/pi-subagents/references/constraints-and-recipes.md +5 -4
- package/dist/skills/pi-subagents/references/execution-controls.md +15 -14
- package/dist/skills/pi-subagents/references/management-authoring-rpc.md +5 -5
- package/dist/skills/pi-subagents/references/prompting-and-roles.md +46 -26
- package/docs/settings.md +6 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to `@selesai/code` will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.8.7] - 2026-08-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **Pi Subagents updated through upstream 0.50.0.** The bundled extension advances through upstream 0.49/0.50 (single-child `{ agent, task }` runs, per-tool-call `toolTimeoutMs` with config/env precedence, `debug.run` inspection, builtin role overrides with `tools: "inherit"`, resumable retained-child listings, indexed async result inboxes) with the Selesai fork layers on top: branding, `.selesai` config routing, `SELESAI_SUBAGENT*` env vars, declarative `chain`/`tasks`/`clarify` modes, workflow auto-relaunch wiring, and same-model retry for transient provider errors.
|
|
11
|
+
- **`model-prompt-injector` extension.** Bundled and registered in the extensions manifest, it injects model-specific prompts into the system prompt when the active model matches a configured rule (provider/model globs, bare id/name patterns, first-match-wins, enabled toggle). Modes: `prepend`, `append`, or `replace`.
|
|
12
|
+
- **Wire-level tool schema pruning.** `pruneToolDescriptions` (default `true`) strips every per-field `description` from tool input schemas before they reach the model, keeping one-line tool descriptions. Stripped schemas are memoized and never mutated; UI/docs definitions keep full descriptions; `keepParameterDescriptions` opts a tool out.
|
|
13
|
+
- **Compact subagent tool description by default.** `toolDescriptionMode` now defaults to `"compact"` (~3.2 KB vs ~6.3 KB per model turn), and a missing/invalid custom description file falls back to compact.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **Slimmer default system prompt.** The always-on Pi-docs block shrank from 8 lines to 2 (paths only); docs are still read on demand when the user asks about pi.
|
|
17
|
+
- **Bundled defaults** now set `pruneToolDescriptions: true`.
|
|
18
|
+
- **Test coverage.** `src/extensions` (pi-* excluded) now enforces 100% stmt/branch/func/line coverage via vitest thresholds; `safeMaxTokens()` clamps missing/oversized context-window metadata to a sane default.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Subagent prompt-runtime suite passes inside subagent child environments.** The watchdog-lifecycle test now scrubs the steer env vars (`SELESAI_SUBAGENT_STEER_*`) before registering, so the steering inbox's extra `agent_end` handler cannot skew the handler count.
|
|
22
|
+
|
|
7
23
|
## [0.8.6] - 2026-08-15
|
|
8
24
|
|
|
9
25
|
### Added
|
|
@@ -35,7 +35,7 @@ import { CURRENT_SESSION_VERSION, getLatestCompactionEntry } from "./session-man
|
|
|
35
35
|
import { createSyntheticSourceInfo } from "./source-info.js";
|
|
36
36
|
import { buildSystemPrompt } from "./system-prompt.js";
|
|
37
37
|
import { createLocalBashOperations } from "./tools/bash.js";
|
|
38
|
-
import { createAllToolDefinitions } from "./tools/index.js";
|
|
38
|
+
import { createAllToolDefinitions, stripToolParameterDescriptions } from "./tools/index.js";
|
|
39
39
|
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
|
|
40
40
|
import { addUsageToTotals, createUsageTotals } from "./usage-totals.js";
|
|
41
41
|
/** Parse consecutive skill blocks at the start of a user message. */
|
|
@@ -2056,6 +2056,18 @@ export class AgentSession {
|
|
|
2056
2056
|
toolRegistry.set(tool.name, tool);
|
|
2057
2057
|
}
|
|
2058
2058
|
this._toolRegistry = toolRegistry;
|
|
2059
|
+
if (this.settingsManager.getPruneToolDescriptions()) {
|
|
2060
|
+
const keepDescriptions = new Set();
|
|
2061
|
+
for (const [name, entry] of definitionRegistry) {
|
|
2062
|
+
if (entry.definition.keepParameterDescriptions)
|
|
2063
|
+
keepDescriptions.add(name);
|
|
2064
|
+
}
|
|
2065
|
+
for (const [name, tool] of toolRegistry) {
|
|
2066
|
+
if (!keepDescriptions.has(name)) {
|
|
2067
|
+
toolRegistry.set(name, stripToolParameterDescriptions(tool));
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2059
2071
|
const nextActiveToolNames = (options?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames]).filter((name) => isAllowedTool(name));
|
|
2060
2072
|
if (allowedToolNames) {
|
|
2061
2073
|
for (const toolName of this._toolRegistry.keys()) {
|
|
@@ -353,6 +353,8 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
|
|
|
353
353
|
promptGuidelines?: string[];
|
|
354
354
|
/** Parameter schema (TypeBox) */
|
|
355
355
|
parameters: TParams;
|
|
356
|
+
/** When global pruneToolDescriptions is enabled, keep this tool's per-field parameter descriptions on the wire (already pruned by the tool itself). */
|
|
357
|
+
keepParameterDescriptions?: boolean;
|
|
356
358
|
/** Optional provider-side constrained sampling request for this tool. Set false to explicitly disable it, equivalent to leaving it undefined. */
|
|
357
359
|
constrainedSampling?: false | ConstrainedSamplingConfig;
|
|
358
360
|
/** Controls whether ToolExecutionComponent renders the standard colored shell or the tool renders its own framing. */
|
|
@@ -2,6 +2,17 @@ import { lazyStream, } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import { getApiProvider } from "@earendil-works/pi-ai/compat";
|
|
3
3
|
import { clearConfigValueCache, getConfigValueEnvVarNames, isCommandConfigValue, isConfigValueConfigured, resolveConfigValueOrThrow, resolveHeadersOrThrow, } from "./resolve-config-value.js";
|
|
4
4
|
export const clearApiKeyCache = clearConfigValueCache;
|
|
5
|
+
/** Sane default when a model definition omits maxTokens (kept small so providers never reject it). */
|
|
6
|
+
const DEFAULT_MAX_TOKENS = 16384;
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a safe max output token count. Missing or non-positive values fall back
|
|
9
|
+
* (to a sane default or an inherited value); explicit values are never allowed to
|
|
10
|
+
* exceed the model's context window.
|
|
11
|
+
*/
|
|
12
|
+
function safeMaxTokens(value, contextWindow, fallback = DEFAULT_MAX_TOKENS) {
|
|
13
|
+
const resolved = typeof value === "number" && value > 0 ? value : fallback;
|
|
14
|
+
return Math.min(resolved, Math.max(contextWindow, 1));
|
|
15
|
+
}
|
|
5
16
|
function mergeCompat(base, override) {
|
|
6
17
|
if (!override)
|
|
7
18
|
return base;
|
|
@@ -20,6 +31,7 @@ function mergeCompat(base, override) {
|
|
|
20
31
|
return merged;
|
|
21
32
|
}
|
|
22
33
|
function applyModelOverride(model, override) {
|
|
34
|
+
const contextWindow = override.contextWindow ?? model.contextWindow;
|
|
23
35
|
return {
|
|
24
36
|
...model,
|
|
25
37
|
name: override.name ?? model.name,
|
|
@@ -37,8 +49,8 @@ function applyModelOverride(model, override) {
|
|
|
37
49
|
tiers: override.cost.tiers ?? model.cost.tiers,
|
|
38
50
|
}
|
|
39
51
|
: model.cost,
|
|
40
|
-
contextWindow
|
|
41
|
-
maxTokens: override.maxTokens ?? model.maxTokens,
|
|
52
|
+
contextWindow,
|
|
53
|
+
maxTokens: safeMaxTokens(override.maxTokens ?? model.maxTokens, contextWindow, model.maxTokens),
|
|
42
54
|
compat: mergeCompat(model.compat, override.compat),
|
|
43
55
|
};
|
|
44
56
|
}
|
|
@@ -56,6 +68,7 @@ function modelFromJson(providerId, definition, providerConfig, defaults) {
|
|
|
56
68
|
if (definition.maxTokens !== undefined && definition.maxTokens <= 0) {
|
|
57
69
|
throw new Error(`Provider ${providerId}, model ${definition.id}: invalid maxTokens`);
|
|
58
70
|
}
|
|
71
|
+
const contextWindow = definition.contextWindow ?? 128000;
|
|
59
72
|
return {
|
|
60
73
|
id: definition.id,
|
|
61
74
|
name: definition.name ?? definition.id,
|
|
@@ -66,8 +79,8 @@ function modelFromJson(providerId, definition, providerConfig, defaults) {
|
|
|
66
79
|
thinkingLevelMap: definition.thinkingLevelMap,
|
|
67
80
|
input: (definition.input ?? ["text"]),
|
|
68
81
|
cost: definition.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
69
|
-
contextWindow
|
|
70
|
-
maxTokens: definition.maxTokens
|
|
82
|
+
contextWindow,
|
|
83
|
+
maxTokens: safeMaxTokens(definition.maxTokens, contextWindow),
|
|
71
84
|
headers: undefined,
|
|
72
85
|
compat: mergeCompat(providerConfig.compat, definition.compat),
|
|
73
86
|
};
|
|
@@ -77,6 +77,7 @@ export interface Settings {
|
|
|
77
77
|
branchSummary?: BranchSummarySettings;
|
|
78
78
|
retry?: RetrySettings;
|
|
79
79
|
hideThinkingBlock?: boolean;
|
|
80
|
+
pruneToolDescriptions?: boolean;
|
|
80
81
|
showCacheMissNotices?: boolean;
|
|
81
82
|
externalEditor?: string;
|
|
82
83
|
shellPath?: string;
|
|
@@ -236,6 +237,7 @@ export declare class SettingsManager {
|
|
|
236
237
|
};
|
|
237
238
|
getWebSocketConnectTimeoutMs(): number | undefined;
|
|
238
239
|
getHideThinkingBlock(): boolean;
|
|
240
|
+
getPruneToolDescriptions(): boolean;
|
|
239
241
|
getShowCacheMissNotices(): boolean;
|
|
240
242
|
getExternalEditorCommand(): string;
|
|
241
243
|
setHideThinkingBlock(hide: boolean): void;
|
|
@@ -598,6 +598,9 @@ export class SettingsManager {
|
|
|
598
598
|
getHideThinkingBlock() {
|
|
599
599
|
return this.settings.hideThinkingBlock ?? false;
|
|
600
600
|
}
|
|
601
|
+
getPruneToolDescriptions() {
|
|
602
|
+
return this.settings.pruneToolDescriptions ?? true;
|
|
603
|
+
}
|
|
601
604
|
getShowCacheMissNotices() {
|
|
602
605
|
return this.settings.showCacheMissNotices ?? false;
|
|
603
606
|
}
|
|
@@ -103,14 +103,8 @@ In addition to the tools above, you may have access to other custom tools depend
|
|
|
103
103
|
Guidelines:
|
|
104
104
|
${guidelines}
|
|
105
105
|
|
|
106
|
-
Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI):
|
|
107
|
-
-
|
|
108
|
-
- Additional docs: ${docsPath}
|
|
109
|
-
- Examples: ${examplesPath} (extensions, custom tools, SDK)
|
|
110
|
-
- When reading pi docs or examples, resolve docs/... under Additional docs and examples/... under Examples, not the current working directory
|
|
111
|
-
- When asked about: extensions (docs/extensions.md, examples/extensions/), themes (docs/themes.md), skills (docs/skills.md), prompt templates (docs/prompt-templates.md), TUI components (docs/tui.md), keybindings (docs/keybindings.md), SDK integrations (docs/sdk.md), custom providers (docs/custom-provider.md), adding models (docs/models.md), pi packages (docs/packages.md), environment variables (docs/environment-variables.md)
|
|
112
|
-
- When working on pi topics, read the docs and examples, and follow .md cross-references before implementing
|
|
113
|
-
- Always read pi .md files completely and follow links to related docs (e.g., tui.md for TUI API details)`;
|
|
106
|
+
Pi documentation (read only when the user asks about pi itself, its SDK, extensions, themes, skills, or TUI): main docs ${readmePath}, additional docs ${docsPath}, examples ${examplesPath}.
|
|
107
|
+
When working on pi topics, resolve docs/... and examples/... under those paths (not cwd), read the relevant .md files completely, and follow cross-references before implementing.`;
|
|
114
108
|
if (appendSection) {
|
|
115
109
|
prompt += appendSection;
|
|
116
110
|
}
|
|
@@ -12,6 +12,12 @@ describe("buildSystemPrompt", () => {
|
|
|
12
12
|
expect(prompt).toMatch(/Guidelines:/);
|
|
13
13
|
expect(prompt).toContain("Current working directory: /tmp/project");
|
|
14
14
|
});
|
|
15
|
+
it("keeps the collapsed Pi-docs block without the per-topic filename map", () => {
|
|
16
|
+
const prompt = buildSystemPrompt({ cwd: "/tmp/project" });
|
|
17
|
+
expect(prompt).toContain("Pi documentation");
|
|
18
|
+
expect(prompt).not.toContain("When asked about:");
|
|
19
|
+
expect(prompt).not.toContain("(docs/extensions.md");
|
|
20
|
+
});
|
|
15
21
|
it("keeps the default tool snippets, skills, and agents XML read-gated", () => {
|
|
16
22
|
const skill = {
|
|
17
23
|
name: "test-skill",
|
|
@@ -5,6 +5,7 @@ export { createFindTool, createFindToolDefinition, type FindOperations, type Fin
|
|
|
5
5
|
export { createGrepTool, createGrepToolDefinition, type GrepOperations, type GrepToolDetails, type GrepToolInput, type GrepToolOptions, } from "./grep.ts";
|
|
6
6
|
export { createLsTool, createLsToolDefinition, type LsOperations, type LsToolDetails, type LsToolInput, type LsToolOptions, } from "./ls.ts";
|
|
7
7
|
export { createReadTool, createReadToolDefinition, type ReadOperations, type ReadToolDetails, type ReadToolInput, type ReadToolOptions, } from "./read.ts";
|
|
8
|
+
export { stripSchemaDescriptions, stripToolParameterDescriptions, } from "./schema-prune.ts";
|
|
8
9
|
export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationOptions, type TruncationResult, truncateHead, truncateLine, truncateTail, } from "./truncate.ts";
|
|
9
10
|
export { createWriteTool, createWriteToolDefinition, type WriteOperations, type WriteToolInput, type WriteToolOptions, } from "./write.ts";
|
|
10
11
|
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
package/dist/core/tools/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { createFindTool, createFindToolDefinition, } from "./find.js";
|
|
|
5
5
|
export { createGrepTool, createGrepToolDefinition, } from "./grep.js";
|
|
6
6
|
export { createLsTool, createLsToolDefinition, } from "./ls.js";
|
|
7
7
|
export { createReadTool, createReadToolDefinition, } from "./read.js";
|
|
8
|
+
export { stripSchemaDescriptions, stripToolParameterDescriptions, } from "./schema-prune.js";
|
|
8
9
|
export { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead, truncateLine, truncateTail, } from "./truncate.js";
|
|
9
10
|
export { createWriteTool, createWriteToolDefinition, } from "./write.js";
|
|
10
11
|
import { createBashTool, createBashToolDefinition } from "./bash.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AgentTool } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import type { TSchema } from "typebox";
|
|
3
|
+
/**
|
|
4
|
+
* Structural deep clone that omits every `description` key at any depth while
|
|
5
|
+
* preserving all other own keys (including TypeBox internals such as `~kind`
|
|
6
|
+
* and `~optional` symbols and function-valued keys). Uses Reflect.ownKeys +
|
|
7
|
+
* property descriptors so non-enumerable metadata survives; never mutates the
|
|
8
|
+
* input.
|
|
9
|
+
*/
|
|
10
|
+
export declare function stripSchemaDescriptions<T>(value: T): T;
|
|
11
|
+
/**
|
|
12
|
+
* Returns a shallow copy of the tool with its parameter schema descriptions
|
|
13
|
+
* stripped. Top-level tool fields (name, description, execute, ...) are
|
|
14
|
+
* untouched.
|
|
15
|
+
*/
|
|
16
|
+
export declare function stripToolParameterDescriptions<T extends TSchema>(tool: AgentTool<T>): AgentTool<T>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Memoized stripped clones, keyed by the original schema root. The input is
|
|
2
|
+
// never mutated; repeat calls return the same cached clone.
|
|
3
|
+
const strippedSchemaCache = new WeakMap();
|
|
4
|
+
/**
|
|
5
|
+
* Structural deep clone that omits every `description` key at any depth while
|
|
6
|
+
* preserving all other own keys (including TypeBox internals such as `~kind`
|
|
7
|
+
* and `~optional` symbols and function-valued keys). Uses Reflect.ownKeys +
|
|
8
|
+
* property descriptors so non-enumerable metadata survives; never mutates the
|
|
9
|
+
* input.
|
|
10
|
+
*/
|
|
11
|
+
export function stripSchemaDescriptions(value) {
|
|
12
|
+
if (!value || typeof value !== "object")
|
|
13
|
+
return value;
|
|
14
|
+
const cached = strippedSchemaCache.get(value);
|
|
15
|
+
if (cached !== undefined)
|
|
16
|
+
return cached;
|
|
17
|
+
const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
|
|
18
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
19
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
20
|
+
if (!descriptor)
|
|
21
|
+
continue;
|
|
22
|
+
if (key === "description")
|
|
23
|
+
continue;
|
|
24
|
+
if ("value" in descriptor) {
|
|
25
|
+
descriptor.value = stripSchemaDescriptions(descriptor.value);
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(result, key, descriptor);
|
|
28
|
+
}
|
|
29
|
+
strippedSchemaCache.set(value, result);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Returns a shallow copy of the tool with its parameter schema descriptions
|
|
34
|
+
* stripped. Top-level tool fields (name, description, execute, ...) are
|
|
35
|
+
* untouched.
|
|
36
|
+
*/
|
|
37
|
+
export function stripToolParameterDescriptions(tool) {
|
|
38
|
+
return { ...tool, parameters: stripSchemaDescriptions(tool.parameters) };
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
|
+
import { Value } from "typebox/value";
|
|
4
|
+
import { SettingsManager } from "../settings-manager.js";
|
|
5
|
+
import { stripSchemaDescriptions, stripToolParameterDescriptions } from "./schema-prune.js";
|
|
6
|
+
describe("stripSchemaDescriptions", () => {
|
|
7
|
+
it("removes description keys at all depths (properties, anyOf arrays, items)", () => {
|
|
8
|
+
const schema = {
|
|
9
|
+
type: "object",
|
|
10
|
+
description: "root",
|
|
11
|
+
properties: {
|
|
12
|
+
name: { type: "string", description: "the name" },
|
|
13
|
+
tags: {
|
|
14
|
+
type: "array",
|
|
15
|
+
description: "the tags",
|
|
16
|
+
items: { type: "string", description: "a tag" },
|
|
17
|
+
},
|
|
18
|
+
choice: {
|
|
19
|
+
anyOf: [{ type: "string", description: "string branch" }, { type: "number" }],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const stripped = stripSchemaDescriptions(schema);
|
|
24
|
+
expect("description" in stripped).toBe(false);
|
|
25
|
+
expect("description" in stripped.properties).toBe(false);
|
|
26
|
+
expect("description" in stripped.properties.tags).toBe(false);
|
|
27
|
+
expect("description" in stripped.properties.tags.items).toBe(false);
|
|
28
|
+
expect("description" in stripped.properties.choice.anyOf[0]).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it("preserves non-description keys such as type, enum, required, minimum, additionalProperties", () => {
|
|
31
|
+
const schema = {
|
|
32
|
+
type: "object",
|
|
33
|
+
properties: {
|
|
34
|
+
level: { type: "string", enum: ["a", "b"], description: "level desc" },
|
|
35
|
+
count: { type: "integer", minimum: 1, description: "count desc" },
|
|
36
|
+
},
|
|
37
|
+
required: ["level"],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
};
|
|
40
|
+
const stripped = stripSchemaDescriptions(schema);
|
|
41
|
+
expect(stripped).toEqual({
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
level: { type: "string", enum: ["a", "b"] },
|
|
45
|
+
count: { type: "integer", minimum: 1 },
|
|
46
|
+
},
|
|
47
|
+
required: ["level"],
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
it("never mutates the input schema", () => {
|
|
52
|
+
const schema = {
|
|
53
|
+
type: "object",
|
|
54
|
+
properties: { name: { type: "string", description: "the name" } },
|
|
55
|
+
};
|
|
56
|
+
const snapshot = structuredClone(schema);
|
|
57
|
+
stripSchemaDescriptions(schema);
|
|
58
|
+
expect(schema).toEqual(snapshot);
|
|
59
|
+
expect(schema.properties.name.description).toBe("the name");
|
|
60
|
+
});
|
|
61
|
+
it("memoizes stripped clones per original root", () => {
|
|
62
|
+
const schema = {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: { name: { type: "string", description: "the name" } },
|
|
65
|
+
};
|
|
66
|
+
const first = stripSchemaDescriptions(schema);
|
|
67
|
+
const second = stripSchemaDescriptions(schema);
|
|
68
|
+
expect(first).toBe(second);
|
|
69
|
+
});
|
|
70
|
+
it("keeps real TypeBox schemas valid after stripping (symbols survive)", () => {
|
|
71
|
+
const schema = Type.Object({
|
|
72
|
+
name: Type.String({ description: "the name" }),
|
|
73
|
+
age: Type.Optional(Type.Integer({ minimum: 0, description: "the age" })),
|
|
74
|
+
});
|
|
75
|
+
const stripped = stripSchemaDescriptions(schema);
|
|
76
|
+
expect(Value.Check(schema, { name: "pi", age: 3 })).toBe(true);
|
|
77
|
+
expect(Value.Check(stripped, { name: "pi", age: 3 })).toBe(true);
|
|
78
|
+
expect(Value.Check(stripped, { name: "pi", age: -1 })).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
describe("stripToolParameterDescriptions", () => {
|
|
82
|
+
it("keeps the tool description and name while stripping parameter descriptions", () => {
|
|
83
|
+
const tool = {
|
|
84
|
+
name: "read",
|
|
85
|
+
label: "Read",
|
|
86
|
+
description: "Read a file",
|
|
87
|
+
parameters: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: { path: { type: "string", description: "file path" } },
|
|
90
|
+
},
|
|
91
|
+
execute: () => Promise.resolve({ content: [], details: {} }),
|
|
92
|
+
};
|
|
93
|
+
const stripped = stripToolParameterDescriptions(tool);
|
|
94
|
+
expect(stripped).not.toBe(tool);
|
|
95
|
+
expect(stripped.name).toBe("read");
|
|
96
|
+
expect(stripped.description).toBe("Read a file");
|
|
97
|
+
expect("description" in stripped.parameters.properties.path).toBe(false);
|
|
98
|
+
// Original tool is untouched.
|
|
99
|
+
expect("description" in tool.parameters.properties.path).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("pruneToolDescriptions setting", () => {
|
|
103
|
+
it("defaults to true", () => {
|
|
104
|
+
const manager = SettingsManager.inMemory();
|
|
105
|
+
expect(manager.getPruneToolDescriptions()).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it("respects explicit true and false", () => {
|
|
108
|
+
expect(SettingsManager.inMemory({ pruneToolDescriptions: true }).getPruneToolDescriptions()).toBe(true);
|
|
109
|
+
expect(SettingsManager.inMemory({ pruneToolDescriptions: false }).getPruneToolDescriptions()).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rules": [
|
|
3
|
+
{
|
|
4
|
+
"match": ["deepseek-v4-pro"],
|
|
5
|
+
"mode": "prepend",
|
|
6
|
+
"prompt": "text: You are a helpful software engineer assistant.\n\ndescription: Bootstrap with shell/read, then expose the full Standard tool catalog after the first durable tool call.\n\nwhen you thought, start with `we need...`",
|
|
7
|
+
"enabled": true
|
|
8
|
+
}
|
|
9
|
+
]
|
|
10
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { ExtensionAPI } from "@selesai/code";
|
|
3
|
+
import modelPromptInjector, {
|
|
4
|
+
findRule,
|
|
5
|
+
patternMatches,
|
|
6
|
+
type InjectConfig,
|
|
7
|
+
type InjectRule,
|
|
8
|
+
type ModelRef,
|
|
9
|
+
} from "./index.ts";
|
|
10
|
+
|
|
11
|
+
const model = (provider: string, id: string, name = id): ModelRef => ({ provider, id, name });
|
|
12
|
+
|
|
13
|
+
function harness(config: InjectConfig, activeModel: ModelRef | undefined) {
|
|
14
|
+
const handlers: Record<string, Function> = {};
|
|
15
|
+
const pi = {
|
|
16
|
+
on: vi.fn((event: string, handler: Function) => {
|
|
17
|
+
handlers[event] = handler;
|
|
18
|
+
}),
|
|
19
|
+
} as unknown as ExtensionAPI;
|
|
20
|
+
const ctx = {
|
|
21
|
+
model: activeModel,
|
|
22
|
+
ui: { setStatus: vi.fn() },
|
|
23
|
+
};
|
|
24
|
+
modelPromptInjector(pi, config);
|
|
25
|
+
return { handlers, ctx };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("patternMatches", () => {
|
|
29
|
+
it("matches provider/model exactly, case-insensitively", () => {
|
|
30
|
+
expect(patternMatches("deepseek/deepseek-v4-pro", model("DeepSeek", "deepseek-v4-pro"))).toBe(true);
|
|
31
|
+
expect(patternMatches("deepseek/deepseek-v4-pro", model("openrouter", "deepseek-v4-pro"))).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("supports wildcards in provider and id", () => {
|
|
35
|
+
expect(patternMatches("*/deepseek-v4-pro", model("openrouter", "deepseek-v4-pro"))).toBe(true);
|
|
36
|
+
expect(patternMatches("deepseek/*", model("deepseek", "deepseek-v4-pro"))).toBe(true);
|
|
37
|
+
expect(patternMatches("deepseek/*", model("deepseek", "deepseek-chat"))).toBe(true);
|
|
38
|
+
expect(patternMatches("deepseek/*", model("openai", "gpt-5"))).toBe(false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("bare patterns match id or display name", () => {
|
|
42
|
+
expect(patternMatches("deepseek-v4-pro", model("deepseek", "deepseek-v4-pro"))).toBe(true);
|
|
43
|
+
expect(patternMatches("gpt-*", model("openai", "gpt-5", "GPT-5"))).toBe(true);
|
|
44
|
+
expect(patternMatches("deepseek*", model("deepseek", "deepseek-v4-pro"))).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("matches * against any model", () => {
|
|
48
|
+
expect(patternMatches("*", model("openai", "gpt-5"))).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("findRule", () => {
|
|
53
|
+
const rules: InjectRule[] = [
|
|
54
|
+
{ match: ["deepseek/*"], prompt: "A" },
|
|
55
|
+
{ match: ["*/deepseek-v4-pro"], prompt: "B" },
|
|
56
|
+
{ match: ["*"], prompt: "C", enabled: false },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
it("first matching rule wins", () => {
|
|
60
|
+
expect(findRule(rules, model("deepseek", "deepseek-v4-pro"))?.prompt).toBe("A");
|
|
61
|
+
expect(findRule(rules, model("openrouter", "deepseek-v4-pro"))?.prompt).toBe("B");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("skips disabled rules", () => {
|
|
65
|
+
expect(findRule([{ match: ["*"], prompt: "C", enabled: false }], model("openai", "gpt-5"))).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("before_agent_start", () => {
|
|
70
|
+
it("appends the rule prompt to the system prompt", async () => {
|
|
71
|
+
const { handlers, ctx } = harness({ rules: [{ match: ["deepseek/*"], prompt: "EXTRA" }] }, model("deepseek", "deepseek-chat"));
|
|
72
|
+
const result = await handlers["before_agent_start"]({ systemPrompt: "BASE" }, ctx);
|
|
73
|
+
expect(result).toEqual({ systemPrompt: "BASE\n\nEXTRA" });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("prepends the rule prompt to the top of the system prompt", async () => {
|
|
77
|
+
const { handlers, ctx } = harness(
|
|
78
|
+
{ rules: [{ match: ["deepseek/*"], prompt: "TOP", mode: "prepend" }] },
|
|
79
|
+
model("deepseek", "deepseek-chat"),
|
|
80
|
+
);
|
|
81
|
+
const result = await handlers["before_agent_start"]({ systemPrompt: "BASE" }, ctx);
|
|
82
|
+
expect(result).toEqual({ systemPrompt: "TOP\n\nBASE" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("replaces the system prompt in replace mode", async () => {
|
|
86
|
+
const { handlers, ctx } = harness(
|
|
87
|
+
{ rules: [{ match: ["deepseek/*"], prompt: "ONLY ME", mode: "replace" }] },
|
|
88
|
+
model("deepseek", "deepseek-chat"),
|
|
89
|
+
);
|
|
90
|
+
const result = await handlers["before_agent_start"]({ systemPrompt: "BASE" }, ctx);
|
|
91
|
+
expect(result).toEqual({ systemPrompt: "ONLY ME" });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns undefined when no rule matches", async () => {
|
|
95
|
+
const { handlers, ctx } = harness(
|
|
96
|
+
{ rules: [{ match: ["deepseek/*"], prompt: "EXTRA" }] },
|
|
97
|
+
model("openai", "gpt-5"),
|
|
98
|
+
);
|
|
99
|
+
const result = await handlers["before_agent_start"]({ systemPrompt: "BASE" }, ctx);
|
|
100
|
+
expect(result).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("model_select", () => {
|
|
105
|
+
it("shows status when the selected model matches, clears otherwise", async () => {
|
|
106
|
+
const { handlers, ctx } = harness({ rules: [{ match: ["deepseek/*"], prompt: "EXTRA" }] }, undefined);
|
|
107
|
+
await handlers["model_select"]({ model: model("deepseek", "deepseek-chat") }, ctx);
|
|
108
|
+
expect(ctx.ui.setStatus).toHaveBeenLastCalledWith("model-prompt-injector", "prompt-inject: active");
|
|
109
|
+
await handlers["model_select"]({ model: model("openai", "gpt-5") }, ctx);
|
|
110
|
+
expect(ctx.ui.setStatus).toHaveBeenLastCalledWith("model-prompt-injector", undefined);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* model-prompt-injector
|
|
3
|
+
*
|
|
4
|
+
* Injects a model-specific prompt into the system prompt whenever the active
|
|
5
|
+
* model matches a rule in config.json (sibling of this file).
|
|
6
|
+
*
|
|
7
|
+
* Config lives only here in host code — nothing is exposed to user settings.
|
|
8
|
+
* `/reload` re-reads it.
|
|
9
|
+
*
|
|
10
|
+
* config.json format:
|
|
11
|
+
* {
|
|
12
|
+
* "rules": [
|
|
13
|
+
* {
|
|
14
|
+
* "match": ["provider/model", "provider/*", "<any>/model", "bare-name", "*"],
|
|
15
|
+
* "mode": "prepend" | "append" | "replace", // default: "append"
|
|
16
|
+
* "prompt": "...",
|
|
17
|
+
* "enabled": true // default: true
|
|
18
|
+
* }
|
|
19
|
+
* ]
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* Matching (case-insensitive, "*" is a wildcard):
|
|
23
|
+
* - "provider/model" — glob against `<provider>/<id>`.
|
|
24
|
+
* - bare pattern (no "/") — glob against model id OR display name.
|
|
25
|
+
* - First matching rule wins; later rules are ignored.
|
|
26
|
+
* - mode "prepend" puts the prompt at the very top of the system prompt;
|
|
27
|
+
* mode "append" adds it to the end; mode "replace" replaces the whole system prompt.
|
|
28
|
+
*/
|
|
29
|
+
import { readFileSync } from "node:fs";
|
|
30
|
+
import { dirname, join } from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import type { ExtensionAPI } from "@selesai/code";
|
|
33
|
+
|
|
34
|
+
export type InjectMode = "prepend" | "append" | "replace";
|
|
35
|
+
|
|
36
|
+
export interface InjectRule {
|
|
37
|
+
match: string[];
|
|
38
|
+
prompt: string;
|
|
39
|
+
mode?: InjectMode;
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface InjectConfig {
|
|
44
|
+
rules: InjectRule[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Model shape needed for matching (structurally compatible with pi's Model). */
|
|
48
|
+
export interface ModelRef {
|
|
49
|
+
provider: string;
|
|
50
|
+
id: string;
|
|
51
|
+
name: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const CONFIG_PATH = join(dirname(fileURLToPath(import.meta.url)), "config.json");
|
|
55
|
+
|
|
56
|
+
function globToRegex(glob: string): RegExp {
|
|
57
|
+
const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
58
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function patternMatches(pattern: string, model: ModelRef): boolean {
|
|
62
|
+
const slash = pattern.indexOf("/");
|
|
63
|
+
if (slash >= 0) {
|
|
64
|
+
return (
|
|
65
|
+
globToRegex(pattern.slice(0, slash)).test(model.provider) &&
|
|
66
|
+
globToRegex(pattern.slice(slash + 1)).test(model.id)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return globToRegex(pattern).test(model.id) || globToRegex(pattern).test(model.name);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function findRule(rules: readonly InjectRule[], model: ModelRef): InjectRule | undefined {
|
|
73
|
+
for (const rule of rules) {
|
|
74
|
+
if (rule.enabled === false) continue;
|
|
75
|
+
if (rule.match.some((pattern) => patternMatches(pattern, model))) return rule;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function loadConfig(path: string = CONFIG_PATH): InjectConfig {
|
|
81
|
+
const fail = (message: string): InjectConfig => {
|
|
82
|
+
console.error(`[model-prompt-injector] ${message}`);
|
|
83
|
+
return { rules: [] };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let raw: unknown;
|
|
87
|
+
try {
|
|
88
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return fail(`cannot load ${path}: ${(error as Error).message}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (typeof raw !== "object" || raw === null || !Array.isArray((raw as { rules?: unknown }).rules)) {
|
|
94
|
+
return fail(`invalid config: expected { "rules": [...] }`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const rules: InjectRule[] = [];
|
|
98
|
+
for (const [index, entry] of (raw as { rules: unknown[] }).rules.entries()) {
|
|
99
|
+
const label = `rule #${index + 1}`;
|
|
100
|
+
if (typeof entry !== "object" || entry === null) {
|
|
101
|
+
console.error(`[model-prompt-injector] ${label} is not an object — skipped`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
const rule = entry as Partial<InjectRule>;
|
|
105
|
+
if (!Array.isArray(rule.match) || rule.match.length === 0 || !rule.match.every((m) => typeof m === "string")) {
|
|
106
|
+
console.error(`[model-prompt-injector] ${label} needs a non-empty "match" string array — skipped`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (typeof rule.prompt !== "string" || rule.prompt.trim() === "") {
|
|
110
|
+
console.error(`[model-prompt-injector] ${label} needs a non-empty "prompt" string — skipped`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (rule.mode !== undefined && rule.mode !== "prepend" && rule.mode !== "append" && rule.mode !== "replace") {
|
|
114
|
+
console.error(`[model-prompt-injector] ${label}: "mode" must be "prepend", "append" or "replace" — skipped`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
rules.push({
|
|
118
|
+
match: rule.match,
|
|
119
|
+
prompt: rule.prompt,
|
|
120
|
+
mode: rule.mode ?? "append",
|
|
121
|
+
enabled: rule.enabled ?? true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return { rules };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default function modelPromptInjector(pi: ExtensionAPI, config: InjectConfig = loadConfig()) {
|
|
128
|
+
const ruleFor = (model: ModelRef | undefined): InjectRule | undefined =>
|
|
129
|
+
model ? findRule(config.rules, model) : undefined;
|
|
130
|
+
|
|
131
|
+
const active = config.rules.filter((rule) => rule.enabled !== false);
|
|
132
|
+
if (active.length > 0) {
|
|
133
|
+
console.log(
|
|
134
|
+
`[model-prompt-injector] ${active.length} rule(s) active: ${active
|
|
135
|
+
.map((rule) => rule.match.join(","))
|
|
136
|
+
.join(" | ")}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
141
|
+
const rule = ruleFor(ctx.model);
|
|
142
|
+
if (!rule) return;
|
|
143
|
+
if (rule.mode === "replace") return { systemPrompt: rule.prompt };
|
|
144
|
+
if (rule.mode === "prepend") return { systemPrompt: `${rule.prompt}\n\n${event.systemPrompt}` };
|
|
145
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${rule.prompt}` };
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
pi.on("model_select", async (event, ctx) => {
|
|
149
|
+
ctx.ui.setStatus("model-prompt-injector", ruleFor(event.model) ? "prompt-inject: active" : undefined);
|
|
150
|
+
});
|
|
151
|
+
}
|