@oh-my-pi/pi-coding-agent 8.4.3 → 8.5.0
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 +26 -0
- package/package.json +6 -6
- package/src/cursor.ts +1 -1
- package/src/modes/components/model-selector.ts +43 -14
- package/src/modes/components/tool-execution.ts +1 -3
- package/src/modes/controllers/event-controller.ts +0 -6
- package/src/modes/interactive-mode.ts +1 -20
- package/src/modes/types.ts +0 -1
- package/src/prompts/system/custom-system-prompt.md +14 -0
- package/src/prompts/system/plan-mode-active.md +4 -0
- package/src/prompts/system/system-prompt.md +12 -0
- package/src/prompts/tools/find.md +3 -2
- package/src/prompts/tools/grep.md +1 -1
- package/src/prompts/tools/task.md +1 -0
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +4 -0
- package/src/session/agent-storage.ts +54 -1
- package/src/session/session-manager.ts +29 -2
- package/src/system-prompt.ts +26 -1
- package/src/task/executor.ts +99 -13
- package/src/task/index.ts +58 -11
- package/src/task/template.ts +3 -1
- package/src/task/types.ts +5 -0
- package/src/task/worker-protocol.ts +1 -0
- package/src/task/worker.ts +9 -0
- package/src/tools/bash.ts +1 -3
- package/src/tools/find.ts +74 -150
- package/src/tools/grep.ts +215 -109
- package/src/tools/index.ts +0 -3
- package/src/tools/output-meta.ts +2 -2
- package/src/tools/python.ts +1 -3
- package/src/tools/read.ts +30 -20
- package/src/prompts/tools/enter-plan-mode.md +0 -92
- package/src/tools/enter-plan-mode.ts +0 -76
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [8.5.0] - 2026-01-27
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added subagent support for preloading skill contents into the system prompt instead of listing available skills
|
|
9
|
+
- Added session init entries to capture system prompt, task, tools, and output schema for subagent session logs
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Reduced Task tool progress update overhead to keep the UI responsive during high-volume streaming output
|
|
13
|
+
- Fixed subagent session logs dropping pre-assistant entries (user/task metadata) before the first assistant response
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
- Removed enter-plan-mode tool
|
|
17
|
+
## [8.4.5] - 2026-01-26
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Model usage tracking to record and retrieve most recently used models
|
|
21
|
+
- Model sorting in selector based on usage history
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- Renamed `head_limit` parameter to `limit` in grep and find tools for consistency
|
|
25
|
+
- Added `context` as an alias for the `c` context parameter in grep tool
|
|
26
|
+
- Made hidden files inclusion configurable in find tool via `hidden` parameter (defaults to true)
|
|
27
|
+
- Added support for reading ignore patterns from .gitignore and .ignore files in find tool
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Respected .gitignore rules when filtering find tool results by glob pattern
|
|
5
31
|
## [8.4.2] - 2026-01-25
|
|
6
32
|
|
|
7
33
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.5.0",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -83,11 +83,11 @@
|
|
|
83
83
|
"test": "bun test"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@oh-my-pi/omp-stats": "8.
|
|
87
|
-
"@oh-my-pi/pi-agent-core": "8.
|
|
88
|
-
"@oh-my-pi/pi-ai": "8.
|
|
89
|
-
"@oh-my-pi/pi-tui": "8.
|
|
90
|
-
"@oh-my-pi/pi-utils": "8.
|
|
86
|
+
"@oh-my-pi/omp-stats": "8.5.0",
|
|
87
|
+
"@oh-my-pi/pi-agent-core": "8.5.0",
|
|
88
|
+
"@oh-my-pi/pi-ai": "8.5.0",
|
|
89
|
+
"@oh-my-pi/pi-tui": "8.5.0",
|
|
90
|
+
"@oh-my-pi/pi-utils": "8.5.0",
|
|
91
91
|
"@openai/agents": "^0.4.3",
|
|
92
92
|
"@sinclair/typebox": "^0.34.46",
|
|
93
93
|
"ajv": "^8.17.1",
|
package/src/cursor.ts
CHANGED
|
@@ -170,7 +170,7 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
170
170
|
context: args.context ?? args.contextBefore ?? args.contextAfter ?? undefined,
|
|
171
171
|
ignore_case: args.caseInsensitive || undefined,
|
|
172
172
|
type: args.type || undefined,
|
|
173
|
-
|
|
173
|
+
limit: args.headLimit ?? undefined,
|
|
174
174
|
multiline: args.multiline || undefined,
|
|
175
175
|
});
|
|
176
176
|
return toolResultMessage;
|
|
@@ -75,7 +75,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
75
75
|
private allModels: ModelItem[] = [];
|
|
76
76
|
private filteredModels: ModelItem[] = [];
|
|
77
77
|
private selectedIndex: number = 0;
|
|
78
|
-
private currentModel?: Model<any>;
|
|
79
78
|
private defaultModel?: Model<any>;
|
|
80
79
|
private smolModel?: Model<any>;
|
|
81
80
|
private slowModel?: Model<any>;
|
|
@@ -98,7 +97,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
98
97
|
|
|
99
98
|
constructor(
|
|
100
99
|
tui: TUI,
|
|
101
|
-
|
|
100
|
+
_currentModel: Model<any> | undefined,
|
|
102
101
|
settingsManager: SettingsManager,
|
|
103
102
|
modelRegistry: ModelRegistry,
|
|
104
103
|
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
@@ -109,7 +108,6 @@ export class ModelSelectorComponent extends Container {
|
|
|
109
108
|
super();
|
|
110
109
|
|
|
111
110
|
this.tui = tui;
|
|
112
|
-
this.currentModel = currentModel;
|
|
113
111
|
this.settingsManager = settingsManager;
|
|
114
112
|
this.modelRegistry = modelRegistry;
|
|
115
113
|
this.scopedModels = scopedModels;
|
|
@@ -213,6 +211,44 @@ export class ModelSelectorComponent extends Container {
|
|
|
213
211
|
}
|
|
214
212
|
}
|
|
215
213
|
|
|
214
|
+
private sortModels(models: ModelItem[]): void {
|
|
215
|
+
// Sort: tagged models (default/smol/slow) first, then MRU, then alphabetical
|
|
216
|
+
const mruOrder = this.settingsManager.getStorage()?.getModelUsageOrder() ?? [];
|
|
217
|
+
const mruIndex = new Map(mruOrder.map((key, i) => [key, i]));
|
|
218
|
+
|
|
219
|
+
models.sort((a, b) => {
|
|
220
|
+
const aKey = `${a.provider}/${a.id}`;
|
|
221
|
+
const bKey = `${b.provider}/${b.id}`;
|
|
222
|
+
|
|
223
|
+
// Tagged models first: default (0), smol (1), slow (2), untagged (3)
|
|
224
|
+
const aTag = modelsAreEqual(this.defaultModel, a.model)
|
|
225
|
+
? 0
|
|
226
|
+
: modelsAreEqual(this.smolModel, a.model)
|
|
227
|
+
? 1
|
|
228
|
+
: modelsAreEqual(this.slowModel, a.model)
|
|
229
|
+
? 2
|
|
230
|
+
: 3;
|
|
231
|
+
const bTag = modelsAreEqual(this.defaultModel, b.model)
|
|
232
|
+
? 0
|
|
233
|
+
: modelsAreEqual(this.smolModel, b.model)
|
|
234
|
+
? 1
|
|
235
|
+
: modelsAreEqual(this.slowModel, b.model)
|
|
236
|
+
? 2
|
|
237
|
+
: 3;
|
|
238
|
+
if (aTag !== bTag) return aTag - bTag;
|
|
239
|
+
|
|
240
|
+
// Then MRU order (models in mruIndex come before those not in it)
|
|
241
|
+
const aMru = mruIndex.get(aKey) ?? Number.MAX_SAFE_INTEGER;
|
|
242
|
+
const bMru = mruIndex.get(bKey) ?? Number.MAX_SAFE_INTEGER;
|
|
243
|
+
if (aMru !== bMru) return aMru - bMru;
|
|
244
|
+
|
|
245
|
+
// Finally alphabetical by provider, then id
|
|
246
|
+
const providerCmp = a.provider.localeCompare(b.provider);
|
|
247
|
+
if (providerCmp !== 0) return providerCmp;
|
|
248
|
+
return a.id.localeCompare(b.id);
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
216
252
|
private async loadModels(): Promise<void> {
|
|
217
253
|
let models: ModelItem[];
|
|
218
254
|
|
|
@@ -249,16 +285,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
249
285
|
}
|
|
250
286
|
}
|
|
251
287
|
|
|
252
|
-
|
|
253
|
-
models.sort((a, b) => {
|
|
254
|
-
const aIsCurrent = modelsAreEqual(this.currentModel, a.model);
|
|
255
|
-
const bIsCurrent = modelsAreEqual(this.currentModel, b.model);
|
|
256
|
-
if (aIsCurrent && !bIsCurrent) return -1;
|
|
257
|
-
if (!aIsCurrent && bIsCurrent) return 1;
|
|
258
|
-
const providerCmp = a.provider.localeCompare(b.provider);
|
|
259
|
-
if (providerCmp !== 0) return providerCmp;
|
|
260
|
-
return a.id.localeCompare(b.id);
|
|
261
|
-
});
|
|
288
|
+
this.sortModels(models);
|
|
262
289
|
|
|
263
290
|
this.allModels = models;
|
|
264
291
|
this.filteredModels = models;
|
|
@@ -315,7 +342,9 @@ export class ModelSelectorComponent extends Container {
|
|
|
315
342
|
this.updateTabBar();
|
|
316
343
|
baseModels = this.allModels;
|
|
317
344
|
}
|
|
318
|
-
|
|
345
|
+
const fuzzyMatches = fuzzyFilter(baseModels, query, ({ id, provider }) => `${id} ${provider}`);
|
|
346
|
+
this.sortModels(fuzzyMatches);
|
|
347
|
+
this.filteredModels = fuzzyMatches;
|
|
319
348
|
} else {
|
|
320
349
|
this.filteredModels = baseModels;
|
|
321
350
|
}
|
|
@@ -558,9 +558,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
558
558
|
const context: Record<string, unknown> = {};
|
|
559
559
|
const normalizeTimeoutSeconds = (value: unknown, maxSeconds: number): number | undefined => {
|
|
560
560
|
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
561
|
-
|
|
562
|
-
timeoutSec = Math.max(1, Math.min(maxSeconds, timeoutSec));
|
|
563
|
-
return timeoutSec;
|
|
561
|
+
return Math.max(1, Math.min(maxSeconds, value));
|
|
564
562
|
};
|
|
565
563
|
|
|
566
564
|
if (this.toolName === "bash" && this.result) {
|
|
@@ -247,12 +247,6 @@ export class EventController {
|
|
|
247
247
|
this.ctx.setTodos(details.todos);
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
|
-
if (event.toolName === "enter_plan_mode" && !event.isError) {
|
|
251
|
-
const details = event.result.details as import("../../tools").EnterPlanModeDetails | undefined;
|
|
252
|
-
if (details) {
|
|
253
|
-
await this.ctx.handleEnterPlanModeTool(details);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
250
|
if (event.toolName === "exit_plan_mode" && !event.isError) {
|
|
257
251
|
const details = event.result.details as ExitPlanModeDetails | undefined;
|
|
258
252
|
if (details) {
|
|
@@ -29,7 +29,7 @@ import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
|
|
|
29
29
|
import { HistoryStorage } from "../session/history-storage";
|
|
30
30
|
import type { SessionContext, SessionManager } from "../session/session-manager";
|
|
31
31
|
import { getRecentSessions } from "../session/session-manager";
|
|
32
|
-
import type {
|
|
32
|
+
import type { ExitPlanModeDetails } from "../tools";
|
|
33
33
|
import { setTerminalTitle } from "../utils/title-generator";
|
|
34
34
|
import type { AssistantMessageComponent } from "./components/assistant-message";
|
|
35
35
|
import type { BashExecutionComponent } from "./components/bash-execution";
|
|
@@ -631,25 +631,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
631
631
|
await this.enterPlanMode();
|
|
632
632
|
}
|
|
633
633
|
|
|
634
|
-
async handleEnterPlanModeTool(details: EnterPlanModeDetails): Promise<void> {
|
|
635
|
-
if (this.planModeEnabled) {
|
|
636
|
-
this.showWarning("Plan mode is already active.");
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
const confirmed = await this.showHookConfirm(
|
|
641
|
-
"Enter plan mode?",
|
|
642
|
-
"This enables read-only planning and creates a plan file for approval.",
|
|
643
|
-
);
|
|
644
|
-
if (!confirmed) {
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
const planFilePath = details.planFilePath || this.getPlanFilePath();
|
|
649
|
-
this.planModePlanFilePath = planFilePath;
|
|
650
|
-
await this.enterPlanMode({ planFilePath, workflow: details.workflow });
|
|
651
|
-
}
|
|
652
|
-
|
|
653
634
|
async handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void> {
|
|
654
635
|
if (!this.planModeEnabled) {
|
|
655
636
|
this.showWarning("Plan mode is not active.");
|
package/src/modes/types.ts
CHANGED
|
@@ -178,7 +178,6 @@ export interface InteractiveModeContext {
|
|
|
178
178
|
openExternalEditor(): void;
|
|
179
179
|
registerExtensionShortcuts(): void;
|
|
180
180
|
handlePlanModeCommand(): Promise<void>;
|
|
181
|
-
handleEnterPlanModeTool(details: import("../tools").EnterPlanModeDetails): Promise<void>;
|
|
182
181
|
handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
|
|
183
182
|
|
|
184
183
|
// Hook UI methods
|
|
@@ -43,6 +43,20 @@ Use the read tool to load a skill's file when the task matches its description.
|
|
|
43
43
|
{{/list}}
|
|
44
44
|
</available_skills>
|
|
45
45
|
{{/if}}
|
|
46
|
+
{{#if preloadedSkills.length}}
|
|
47
|
+
The following skills are preloaded in full. Apply their instructions directly.
|
|
48
|
+
|
|
49
|
+
<preloaded_skills>
|
|
50
|
+
{{#list preloadedSkills join="\n"}}
|
|
51
|
+
<skill name="{{name}}">
|
|
52
|
+
<location>skill://{{escapeXml name}}</location>
|
|
53
|
+
<content>
|
|
54
|
+
{{content}}
|
|
55
|
+
</content>
|
|
56
|
+
</skill>
|
|
57
|
+
{{/list}}
|
|
58
|
+
</preloaded_skills>
|
|
59
|
+
{{/if}}
|
|
46
60
|
{{#if rules.length}}
|
|
47
61
|
The following rules define project-specific guidelines and constraints:
|
|
48
62
|
|
|
@@ -19,6 +19,10 @@ Create your plan at `{{planFilePath}}`.
|
|
|
19
19
|
|
|
20
20
|
The plan file is the ONLY file you may write or edit.
|
|
21
21
|
|
|
22
|
+
<important>
|
|
23
|
+
Plan execution runs in a fresh context (session cleared). Make the plan file self-contained: include any requirements, decisions, key findings, and remaining todos needed to continue without prior session history.
|
|
24
|
+
</important>
|
|
25
|
+
|
|
22
26
|
{{#if reentry}}
|
|
23
27
|
## Re-entry
|
|
24
28
|
|
|
@@ -264,6 +264,18 @@ If a skill covers what you're producing, read it before proceeding.
|
|
|
264
264
|
{{/list}}
|
|
265
265
|
</skills>
|
|
266
266
|
{{/if}}
|
|
267
|
+
{{#if preloadedSkills.length}}
|
|
268
|
+
<preloaded_skills>
|
|
269
|
+
The following skills are preloaded in full. Apply their instructions directly.
|
|
270
|
+
|
|
271
|
+
{{#list preloadedSkills join="\n"}}
|
|
272
|
+
<skill name="{{name}}">
|
|
273
|
+
<location>skill://{{escapeXml name}}</location>
|
|
274
|
+
{{content}}
|
|
275
|
+
</skill>
|
|
276
|
+
{{/list}}
|
|
277
|
+
</preloaded_skills>
|
|
278
|
+
{{/if}}
|
|
267
279
|
{{#if rules.length}}
|
|
268
280
|
<rules>
|
|
269
281
|
Rules are local constraints.
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
Fast file pattern matching that works with any codebase size.
|
|
4
4
|
|
|
5
5
|
<instruction>
|
|
6
|
-
- Supports glob patterns like
|
|
6
|
+
- Supports glob patterns like `**/*.js` or `src/**/*.ts`
|
|
7
|
+
- Includes hidden files by default (use `hidden: false` to exclude)
|
|
7
8
|
- Speculatively perform multiple searches in parallel when potentially useful
|
|
8
9
|
</instruction>
|
|
9
10
|
|
|
10
11
|
<output>
|
|
11
|
-
Matching file paths sorted by modification time (most recent first). Results truncated at 1000 entries or 50KB.
|
|
12
|
+
Matching file paths sorted by modification time (most recent first). Results truncated at 1000 entries or 50KB (configurable via `limit`).
|
|
12
13
|
</output>
|
|
13
14
|
|
|
14
15
|
<avoid>
|
|
@@ -16,7 +16,7 @@ Results depend on `output_mode`:
|
|
|
16
16
|
- `count`: Match counts per file
|
|
17
17
|
|
|
18
18
|
In `content` mode, truncated at 100 matches by default (configurable via `limit`).
|
|
19
|
-
For `files_with_matches` and `count` modes, use `
|
|
19
|
+
For `files_with_matches` and `count` modes, use `limit` to truncate results.
|
|
20
20
|
</output>
|
|
21
21
|
|
|
22
22
|
<critical>
|
|
@@ -38,6 +38,7 @@ Agents with `output="structured"` have a fixed schema enforced via frontmatter;
|
|
|
38
38
|
- `id`: Short CamelCase identifier (max 32 chars, e.g., "SessionStore", "LspRefactor")
|
|
39
39
|
- `description`: Short human-readable description of what the task does
|
|
40
40
|
- `args`: Object with keys matching `\{{placeholders}}` in context (always include this, even if empty)
|
|
41
|
+
- `skills`: (optional) Array of skill names to preload into this task's system prompt. When set, the skills index section is omitted and the full SKILL.md contents are embedded.
|
|
41
42
|
- `output`: (optional) JTD schema for structured subagent output (used by the complete tool)
|
|
42
43
|
</parameters>
|
|
43
44
|
|
package/src/sdk.ts
CHANGED
|
@@ -156,6 +156,8 @@ export interface CreateAgentSessionOptions {
|
|
|
156
156
|
|
|
157
157
|
/** Skills. Default: discovered from multiple locations */
|
|
158
158
|
skills?: Skill[];
|
|
159
|
+
/** Skills to inline into the system prompt instead of listing available skills. */
|
|
160
|
+
preloadedSkills?: Skill[];
|
|
159
161
|
/** Context files (AGENTS.md content). Default: discovered walking up from cwd */
|
|
160
162
|
contextFiles?: Array<{ path: string; content: string }>;
|
|
161
163
|
/** Prompt templates. Default: discovered from cwd/.omp/prompts/ + agentDir/prompts/ */
|
|
@@ -993,6 +995,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
993
995
|
const defaultPrompt = await buildSystemPromptInternal({
|
|
994
996
|
cwd,
|
|
995
997
|
skills,
|
|
998
|
+
preloadedSkills: options.preloadedSkills,
|
|
996
999
|
contextFiles,
|
|
997
1000
|
tools,
|
|
998
1001
|
toolNames,
|
|
@@ -1007,6 +1010,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1007
1010
|
return await buildSystemPromptInternal({
|
|
1008
1011
|
cwd,
|
|
1009
1012
|
skills,
|
|
1013
|
+
preloadedSkills: options.preloadedSkills,
|
|
1010
1014
|
contextFiles,
|
|
1011
1015
|
tools,
|
|
1012
1016
|
toolNames,
|
|
@@ -1676,6 +1676,7 @@ export class AgentSession {
|
|
|
1676
1676
|
this.agent.setModel(model);
|
|
1677
1677
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
|
|
1678
1678
|
this.settingsManager.setModelRole(role, `${model.provider}/${model.id}`);
|
|
1679
|
+
this.settingsManager.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
1679
1680
|
|
|
1680
1681
|
// Re-clamp thinking level for new model's capabilities
|
|
1681
1682
|
this.setThinkingLevel(this.thinkingLevel);
|
|
@@ -1694,6 +1695,7 @@ export class AgentSession {
|
|
|
1694
1695
|
|
|
1695
1696
|
this.agent.setModel(model);
|
|
1696
1697
|
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
|
|
1698
|
+
this.settingsManager.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
1697
1699
|
|
|
1698
1700
|
// Re-clamp thinking level for new model's capabilities
|
|
1699
1701
|
this.setThinkingLevel(this.thinkingLevel);
|
|
@@ -1790,6 +1792,7 @@ export class AgentSession {
|
|
|
1790
1792
|
this.agent.setModel(next.model);
|
|
1791
1793
|
this.sessionManager.appendModelChange(`${next.model.provider}/${next.model.id}`);
|
|
1792
1794
|
this.settingsManager.setModelRole("default", `${next.model.provider}/${next.model.id}`);
|
|
1795
|
+
this.settingsManager.getStorage()?.recordModelUsage(`${next.model.provider}/${next.model.id}`);
|
|
1793
1796
|
|
|
1794
1797
|
// Apply thinking level (setThinkingLevel clamps to model capabilities)
|
|
1795
1798
|
this.setThinkingLevel(next.thinkingLevel);
|
|
@@ -1817,6 +1820,7 @@ export class AgentSession {
|
|
|
1817
1820
|
this.agent.setModel(nextModel);
|
|
1818
1821
|
this.sessionManager.appendModelChange(`${nextModel.provider}/${nextModel.id}`);
|
|
1819
1822
|
this.settingsManager.setModelRole("default", `${nextModel.provider}/${nextModel.id}`);
|
|
1823
|
+
this.settingsManager.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
|
1820
1824
|
|
|
1821
1825
|
// Re-clamp thinking level for new model's capabilities
|
|
1822
1826
|
this.setThinkingLevel(this.thinkingLevel);
|
|
@@ -23,6 +23,12 @@ type AuthRow = {
|
|
|
23
23
|
data: string;
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
/** Row shape for model_usage table queries */
|
|
27
|
+
type ModelUsageRow = {
|
|
28
|
+
model_key: string;
|
|
29
|
+
last_used_at: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
26
32
|
/**
|
|
27
33
|
* Auth credential with database row ID for updates/deletes.
|
|
28
34
|
* Wraps AuthCredential with storage metadata.
|
|
@@ -34,7 +40,7 @@ export interface StoredAuthCredential {
|
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/** Bump when schema changes require migration */
|
|
37
|
-
const SCHEMA_VERSION =
|
|
43
|
+
const SCHEMA_VERSION = 3;
|
|
38
44
|
|
|
39
45
|
/**
|
|
40
46
|
* Type guard for plain objects.
|
|
@@ -126,6 +132,9 @@ export class AgentStorage {
|
|
|
126
132
|
private deleteAuthStmt: Statement;
|
|
127
133
|
private deleteAuthByProviderStmt: Statement;
|
|
128
134
|
private countAuthStmt: Statement;
|
|
135
|
+
private upsertModelUsageStmt: Statement;
|
|
136
|
+
private listModelUsageStmt: Statement;
|
|
137
|
+
private modelUsageCache: string[] | null = null;
|
|
129
138
|
|
|
130
139
|
private constructor(dbPath: string) {
|
|
131
140
|
this.ensureDir(dbPath);
|
|
@@ -157,6 +166,13 @@ export class AgentStorage {
|
|
|
157
166
|
this.deleteAuthStmt = this.db.prepare("DELETE FROM auth_credentials WHERE id = ?");
|
|
158
167
|
this.deleteAuthByProviderStmt = this.db.prepare("DELETE FROM auth_credentials WHERE provider = ?");
|
|
159
168
|
this.countAuthStmt = this.db.prepare("SELECT COUNT(*) as count FROM auth_credentials");
|
|
169
|
+
|
|
170
|
+
this.upsertModelUsageStmt = this.db.prepare(
|
|
171
|
+
"INSERT INTO model_usage (model_key, last_used_at) VALUES (?, unixepoch()) ON CONFLICT(model_key) DO UPDATE SET last_used_at = unixepoch()",
|
|
172
|
+
);
|
|
173
|
+
this.listModelUsageStmt = this.db.prepare(
|
|
174
|
+
"SELECT model_key, last_used_at FROM model_usage ORDER BY last_used_at DESC",
|
|
175
|
+
);
|
|
160
176
|
}
|
|
161
177
|
|
|
162
178
|
/**
|
|
@@ -186,6 +202,11 @@ CREATE TABLE IF NOT EXISTS cache (
|
|
|
186
202
|
);
|
|
187
203
|
CREATE INDEX IF NOT EXISTS idx_cache_expires ON cache(expires_at);
|
|
188
204
|
|
|
205
|
+
CREATE TABLE IF NOT EXISTS model_usage (
|
|
206
|
+
model_key TEXT PRIMARY KEY,
|
|
207
|
+
last_used_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
208
|
+
);
|
|
209
|
+
|
|
189
210
|
CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY);
|
|
190
211
|
`);
|
|
191
212
|
|
|
@@ -357,6 +378,38 @@ CREATE TABLE settings (
|
|
|
357
378
|
}
|
|
358
379
|
}
|
|
359
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Records model usage, updating the last-used timestamp.
|
|
383
|
+
* @param modelKey - Model key in "provider/modelId" format
|
|
384
|
+
*/
|
|
385
|
+
recordModelUsage(modelKey: string): void {
|
|
386
|
+
try {
|
|
387
|
+
this.upsertModelUsageStmt.run(modelKey);
|
|
388
|
+
this.modelUsageCache = null;
|
|
389
|
+
} catch (error) {
|
|
390
|
+
logger.warn("AgentStorage failed to record model usage", { modelKey, error: String(error) });
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Gets model keys ordered by most recently used.
|
|
396
|
+
* Results are cached until recordModelUsage is called.
|
|
397
|
+
* @returns Array of model keys ("provider/modelId") in MRU order
|
|
398
|
+
*/
|
|
399
|
+
getModelUsageOrder(): string[] {
|
|
400
|
+
if (this.modelUsageCache) {
|
|
401
|
+
return this.modelUsageCache;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const rows = this.listModelUsageStmt.all() as ModelUsageRow[];
|
|
405
|
+
this.modelUsageCache = rows.map(row => row.model_key);
|
|
406
|
+
return this.modelUsageCache;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
logger.warn("AgentStorage failed to get model usage order", { error: String(error) });
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
360
413
|
/**
|
|
361
414
|
* Checks if any auth credentials exist in storage.
|
|
362
415
|
* @returns True if at least one credential is stored
|
|
@@ -110,6 +110,19 @@ export interface TtsrInjectionEntry extends SessionEntryBase {
|
|
|
110
110
|
injectedRules: string[];
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/** Session init entry - captures initial context for subagent sessions (debugging/replay). */
|
|
114
|
+
export interface SessionInitEntry extends SessionEntryBase {
|
|
115
|
+
type: "session_init";
|
|
116
|
+
/** Full system prompt sent to the model */
|
|
117
|
+
systemPrompt: string;
|
|
118
|
+
/** Initial task/user message */
|
|
119
|
+
task: string;
|
|
120
|
+
/** Tools available to the agent */
|
|
121
|
+
tools: string[];
|
|
122
|
+
/** Output schema if structured output was requested */
|
|
123
|
+
outputSchema?: unknown;
|
|
124
|
+
}
|
|
125
|
+
|
|
113
126
|
/**
|
|
114
127
|
* Custom message entry for extensions to inject messages into LLM context.
|
|
115
128
|
* Use customType to identify your extension's entries.
|
|
@@ -140,7 +153,8 @@ export type SessionEntry =
|
|
|
140
153
|
| CustomEntry
|
|
141
154
|
| CustomMessageEntry
|
|
142
155
|
| LabelEntry
|
|
143
|
-
| TtsrInjectionEntry
|
|
156
|
+
| TtsrInjectionEntry
|
|
157
|
+
| SessionInitEntry;
|
|
144
158
|
|
|
145
159
|
/** Raw file entry (includes header) */
|
|
146
160
|
export type FileEntry = SessionHeader | SessionEntry;
|
|
@@ -1263,7 +1277,7 @@ export class SessionManager {
|
|
|
1263
1277
|
if (this.persistError) throw this.persistError;
|
|
1264
1278
|
|
|
1265
1279
|
const hasAssistant = this.fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
|
|
1266
|
-
if (!hasAssistant) return;
|
|
1280
|
+
if (!hasAssistant && !this.flushed) return;
|
|
1267
1281
|
|
|
1268
1282
|
if (!this.flushed) {
|
|
1269
1283
|
this.flushed = true;
|
|
@@ -1368,6 +1382,19 @@ export class SessionManager {
|
|
|
1368
1382
|
return entry.id;
|
|
1369
1383
|
}
|
|
1370
1384
|
|
|
1385
|
+
/** Append session init metadata (for subagent debugging/replay). Returns entry id. */
|
|
1386
|
+
appendSessionInit(init: { systemPrompt: string; task: string; tools: string[]; outputSchema?: unknown }): string {
|
|
1387
|
+
const entry: SessionInitEntry = {
|
|
1388
|
+
type: "session_init",
|
|
1389
|
+
id: generateId(this.byId),
|
|
1390
|
+
parentId: this.leafId,
|
|
1391
|
+
timestamp: new Date().toISOString(),
|
|
1392
|
+
...init,
|
|
1393
|
+
};
|
|
1394
|
+
this._appendEntry(entry);
|
|
1395
|
+
return entry.id;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1371
1398
|
/** Append a compaction summary as child of current leaf, then advance leaf. Returns entry id. */
|
|
1372
1399
|
appendCompaction<T = unknown>(
|
|
1373
1400
|
summary: string,
|
package/src/system-prompt.ts
CHANGED
|
@@ -23,6 +23,24 @@ interface GitContext {
|
|
|
23
23
|
commits: string;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
type PreloadedSkill = { name: string; content: string };
|
|
27
|
+
|
|
28
|
+
async function loadPreloadedSkillContents(preloadedSkills: Skill[]): Promise<PreloadedSkill[]> {
|
|
29
|
+
const contents = await Promise.all(
|
|
30
|
+
preloadedSkills.map(async skill => {
|
|
31
|
+
try {
|
|
32
|
+
const content = await Bun.file(skill.filePath).text();
|
|
33
|
+
return { name: skill.name, content };
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
36
|
+
throw new Error(`Failed to load skill "${skill.name}" from ${skill.filePath}: ${message}`);
|
|
37
|
+
}
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return contents;
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
/**
|
|
27
45
|
* Load git context for the system prompt.
|
|
28
46
|
* Returns structured git data or null if not in a git repo.
|
|
@@ -643,6 +661,8 @@ export interface BuildSystemPromptOptions {
|
|
|
643
661
|
contextFiles?: Array<{ path: string; content: string; depth?: number }>;
|
|
644
662
|
/** Pre-loaded skills (skips discovery if provided). */
|
|
645
663
|
skills?: Skill[];
|
|
664
|
+
/** Skills to inline into the system prompt instead of listing available skills. */
|
|
665
|
+
preloadedSkills?: Skill[];
|
|
646
666
|
/** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
|
|
647
667
|
rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
|
|
648
668
|
}
|
|
@@ -662,6 +682,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
662
682
|
cwd,
|
|
663
683
|
contextFiles: providedContextFiles,
|
|
664
684
|
skills: providedSkills,
|
|
685
|
+
preloadedSkills: providedPreloadedSkills,
|
|
665
686
|
rules,
|
|
666
687
|
} = options;
|
|
667
688
|
const resolvedCwd = cwd ?? process.cwd();
|
|
@@ -707,13 +728,15 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
707
728
|
const skills =
|
|
708
729
|
providedSkills ??
|
|
709
730
|
(skillsSettings?.enabled !== false ? (await loadSkills({ ...skillsSettings, cwd: resolvedCwd })).skills : []);
|
|
731
|
+
const preloadedSkills = providedPreloadedSkills;
|
|
732
|
+
const preloadedSkillContents = preloadedSkills ? await loadPreloadedSkillContents(preloadedSkills) : [];
|
|
710
733
|
|
|
711
734
|
// Get git context
|
|
712
735
|
const git = await loadGitContext(resolvedCwd);
|
|
713
736
|
|
|
714
737
|
// Filter skills to only include those with read tool
|
|
715
738
|
const hasRead = tools?.has("read");
|
|
716
|
-
const filteredSkills = hasRead ? skills : [];
|
|
739
|
+
const filteredSkills = preloadedSkills === undefined && hasRead ? skills : [];
|
|
717
740
|
|
|
718
741
|
if (resolvedCustomPrompt) {
|
|
719
742
|
return renderPromptTemplate(customSystemPromptTemplate, {
|
|
@@ -724,6 +747,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
724
747
|
agentsMdSearch,
|
|
725
748
|
git,
|
|
726
749
|
skills: filteredSkills,
|
|
750
|
+
preloadedSkills: preloadedSkillContents,
|
|
727
751
|
rules: rules ?? [],
|
|
728
752
|
dateTime,
|
|
729
753
|
cwd: resolvedCwd,
|
|
@@ -738,6 +762,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
738
762
|
agentsMdSearch,
|
|
739
763
|
git,
|
|
740
764
|
skills: filteredSkills,
|
|
765
|
+
preloadedSkills: preloadedSkillContents,
|
|
741
766
|
rules: rules ?? [],
|
|
742
767
|
dateTime,
|
|
743
768
|
cwd: resolvedCwd,
|