@oh-my-pi/pi-coding-agent 13.12.3 → 13.12.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 +34 -0
- package/package.json +7 -7
- package/src/commit/agentic/agent.ts +8 -2
- package/src/config/model-registry.ts +51 -4
- package/src/config/prompt-templates.ts +25 -1
- package/src/config/settings-schema.ts +11 -0
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/slash-commands.ts +8 -2
- package/src/modes/components/settings-defs.ts +11 -0
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/interactive-mode.ts +1 -1
- package/src/sdk.ts +22 -28
- package/src/session/agent-session.ts +83 -7
- package/src/session/session-manager.ts +74 -33
- package/src/task/executor.ts +5 -2
- package/src/tools/output-meta.ts +24 -25
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.12.5] - 2026-03-15
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Automatic discovery of Ollama model context window from model metadata, enabling accurate token limit configuration
|
|
10
|
+
- Added `attribution` option to `PromptOptions` to explicitly control billing/initiator attribution for prompts
|
|
11
|
+
- Added automatic clearing of completed and abandoned todo tasks after ~1 minute
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Ollama model registration now uses discovered context window instead of hardcoded 128000 token default
|
|
16
|
+
- Ollama model maxTokens now respects discovered context window constraints
|
|
17
|
+
- Improved session directory migration to handle legacy absolute paths with double-dash format, automatically relocating them to new canonical locations
|
|
18
|
+
- Enhanced session directory encoding to use `-tmp-` prefix for temporary directories instead of legacy double-dash format for better clarity
|
|
19
|
+
- Updated `SessionManager.create()` to require both `cwd` and `sessionDir` parameters for explicit session directory control
|
|
20
|
+
- Improved session directory naming for temporary working directories using `-tmp-` prefix instead of legacy `--` format
|
|
21
|
+
- Made `cwd` and `sessionDir` fields mutable in SessionManager to support session relocation without type casting
|
|
22
|
+
- Changed subagent prompts to explicitly set `attribution: "agent"` for accurate billing attribution
|
|
23
|
+
- Strip already-completed tasks when restoring session from branch history
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Fixed automatic migration of legacy session directories to new `-tmp-` prefixed naming scheme for temp-root sessions
|
|
28
|
+
|
|
29
|
+
## [13.12.4] - 2026-03-15
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Exposed `settings` instance in `CustomToolContext` for session-specific configuration access
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Improved artifact spill configuration to use session settings with schema defaults as fallback
|
|
37
|
+
- Refactored type annotations for better type safety in tool result handling
|
|
38
|
+
|
|
5
39
|
## [13.12.2] - 2026-03-15
|
|
6
40
|
|
|
7
41
|
### Added
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.12.
|
|
4
|
+
"version": "13.12.5",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.12.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.12.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.12.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.12.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.12.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.12.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.12.5",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.12.5",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.12.5",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.12.5",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.12.5",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.12.5",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -161,11 +161,17 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
|
|
|
161
161
|
let retryCount = 0;
|
|
162
162
|
const needsChangelog = input.requireChangelog && input.changelogTargets.length > 0;
|
|
163
163
|
|
|
164
|
-
await session.prompt(prompt, {
|
|
164
|
+
await session.prompt(prompt, {
|
|
165
|
+
attribution: "agent",
|
|
166
|
+
expandPromptTemplates: false,
|
|
167
|
+
});
|
|
165
168
|
while (retryCount < MAX_RETRIES && !isProposalComplete(state, needsChangelog)) {
|
|
166
169
|
retryCount += 1;
|
|
167
170
|
const reminder = buildReminderMessage(state, needsChangelog, retryCount, MAX_RETRIES);
|
|
168
|
-
await session.prompt(reminder, {
|
|
171
|
+
await session.prompt(reminder, {
|
|
172
|
+
attribution: "agent",
|
|
173
|
+
expandPromptTemplates: false,
|
|
174
|
+
});
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
return state;
|
|
@@ -366,6 +366,12 @@ interface CustomModelsResult {
|
|
|
366
366
|
found: boolean;
|
|
367
367
|
}
|
|
368
368
|
|
|
369
|
+
type OllamaDiscoveredModelMetadata = {
|
|
370
|
+
reasoning: boolean;
|
|
371
|
+
input: ("text" | "image")[];
|
|
372
|
+
contextWindow?: number;
|
|
373
|
+
};
|
|
374
|
+
|
|
369
375
|
/**
|
|
370
376
|
* Resolve an API key config value to an actual key.
|
|
371
377
|
* Checks environment variable first, then treats as literal.
|
|
@@ -376,6 +382,40 @@ function resolveApiKeyConfig(keyConfig: string): string | undefined {
|
|
|
376
382
|
return keyConfig;
|
|
377
383
|
}
|
|
378
384
|
|
|
385
|
+
function toPositiveNumberOrUndefined(value: unknown): number | undefined {
|
|
386
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
387
|
+
return value;
|
|
388
|
+
}
|
|
389
|
+
if (typeof value === "string" && value.trim()) {
|
|
390
|
+
const parsed = Number(value);
|
|
391
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
392
|
+
return parsed;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function extractOllamaContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
399
|
+
const modelInfo = payload.model_info;
|
|
400
|
+
if (isRecord(modelInfo)) {
|
|
401
|
+
for (const [key, value] of Object.entries(modelInfo)) {
|
|
402
|
+
if (key === "context_length" || key.endsWith(".context_length")) {
|
|
403
|
+
const contextWindow = toPositiveNumberOrUndefined(value);
|
|
404
|
+
if (contextWindow !== undefined) {
|
|
405
|
+
return contextWindow;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const parameters = payload.parameters;
|
|
412
|
+
if (typeof parameters !== "string") {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
const match = parameters.match(/(?:^|\n)\s*num_ctx\s+(\d+)\s*(?:$|\n)/m);
|
|
416
|
+
return match ? toPositiveNumberOrUndefined(match[1]) : undefined;
|
|
417
|
+
}
|
|
418
|
+
|
|
379
419
|
function extractGoogleOAuthToken(value: string | undefined): string | undefined {
|
|
380
420
|
if (!isAuthenticated(value)) return undefined;
|
|
381
421
|
try {
|
|
@@ -1096,7 +1136,7 @@ export class ModelRegistry {
|
|
|
1096
1136
|
endpoint: string,
|
|
1097
1137
|
modelId: string,
|
|
1098
1138
|
headers: Record<string, string> | undefined,
|
|
1099
|
-
): Promise<
|
|
1139
|
+
): Promise<OllamaDiscoveredModelMetadata | null> {
|
|
1100
1140
|
const showUrl = `${endpoint}/api/show`;
|
|
1101
1141
|
try {
|
|
1102
1142
|
const response = await fetch(showUrl, {
|
|
@@ -1112,6 +1152,7 @@ export class ModelRegistry {
|
|
|
1112
1152
|
if (!isRecord(payload)) {
|
|
1113
1153
|
return null;
|
|
1114
1154
|
}
|
|
1155
|
+
const contextWindow = extractOllamaContextWindow(payload);
|
|
1115
1156
|
const capabilities = payload.capabilities;
|
|
1116
1157
|
if (Array.isArray(capabilities)) {
|
|
1117
1158
|
const normalized = new Set(
|
|
@@ -1121,15 +1162,21 @@ export class ModelRegistry {
|
|
|
1121
1162
|
return {
|
|
1122
1163
|
reasoning: normalized.has("thinking"),
|
|
1123
1164
|
input: supportsVision ? ["text", "image"] : ["text"],
|
|
1165
|
+
contextWindow,
|
|
1124
1166
|
};
|
|
1125
1167
|
}
|
|
1126
1168
|
if (!isRecord(capabilities)) {
|
|
1127
|
-
return
|
|
1169
|
+
return {
|
|
1170
|
+
reasoning: false,
|
|
1171
|
+
input: ["text"],
|
|
1172
|
+
contextWindow,
|
|
1173
|
+
};
|
|
1128
1174
|
}
|
|
1129
1175
|
const supportsVision = capabilities.vision === true || capabilities.image === true;
|
|
1130
1176
|
return {
|
|
1131
1177
|
reasoning: capabilities.thinking === true,
|
|
1132
1178
|
input: supportsVision ? ["text", "image"] : ["text"],
|
|
1179
|
+
contextWindow,
|
|
1133
1180
|
};
|
|
1134
1181
|
} catch {
|
|
1135
1182
|
return null;
|
|
@@ -1170,8 +1217,8 @@ export class ModelRegistry {
|
|
|
1170
1217
|
reasoning: metadata?.reasoning ?? false,
|
|
1171
1218
|
input: metadata?.input ?? ["text"],
|
|
1172
1219
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
1173
|
-
contextWindow: 128000,
|
|
1174
|
-
maxTokens: 8192,
|
|
1220
|
+
contextWindow: metadata?.contextWindow ?? 128000,
|
|
1221
|
+
maxTokens: Math.min(metadata?.contextWindow ?? Number.POSITIVE_INFINITY, 8192),
|
|
1175
1222
|
headers: providerConfig.headers,
|
|
1176
1223
|
});
|
|
1177
1224
|
});
|
|
@@ -275,6 +275,28 @@ handlebars.registerHelper("hlinefull", (lineNum: unknown, content: unknown): str
|
|
|
275
275
|
return `${ref}:${text}`;
|
|
276
276
|
});
|
|
277
277
|
|
|
278
|
+
const INLINE_ARG_SHELL_PATTERN = /\$(?:ARGUMENTS|@(?:\[\d+(?::\d*)?\])?|\d+)/;
|
|
279
|
+
const INLINE_ARG_TEMPLATE_PATTERN = /\{\{[\s\S]*?(?:\b(?:arguments|ARGUMENTS|args)\b|\barg\s+[^}]+)[\s\S]*?\}\}/;
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Keep the check source-level and cheap: if the template text contains any explicit
|
|
283
|
+
* inline-arg placeholder syntax, do not append the fallback text again.
|
|
284
|
+
*/
|
|
285
|
+
export function templateUsesInlineArgPlaceholders(templateSource: string): boolean {
|
|
286
|
+
return INLINE_ARG_SHELL_PATTERN.test(templateSource) || INLINE_ARG_TEMPLATE_PATTERN.test(templateSource);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function appendInlineArgsFallback(
|
|
290
|
+
rendered: string,
|
|
291
|
+
argsText: string,
|
|
292
|
+
usesInlineArgPlaceholders: boolean,
|
|
293
|
+
): string {
|
|
294
|
+
if (argsText.length === 0 || usesInlineArgPlaceholders) return rendered;
|
|
295
|
+
if (rendered.length === 0) return argsText;
|
|
296
|
+
|
|
297
|
+
return `${rendered}\n\n${argsText}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
278
300
|
export function renderPromptTemplate(template: string, context: TemplateContext = {}): string {
|
|
279
301
|
const compiled = handlebars.compile(template, { noEscape: true, strict: false });
|
|
280
302
|
const rendered = compiled(context ?? {});
|
|
@@ -405,8 +427,10 @@ export function expandPromptTemplate(text: string, templates: PromptTemplate[]):
|
|
|
405
427
|
if (template) {
|
|
406
428
|
const args = parseCommandArgs(argsString);
|
|
407
429
|
const argsText = args.join(" ");
|
|
430
|
+
const usesInlineArgPlaceholders = templateUsesInlineArgPlaceholders(template.content);
|
|
408
431
|
const substituted = substituteArgs(template.content, args);
|
|
409
|
-
|
|
432
|
+
const rendered = renderPromptTemplate(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
|
|
433
|
+
return appendInlineArgsFallback(rendered, argsText, usesInlineArgPlaceholders);
|
|
410
434
|
}
|
|
411
435
|
|
|
412
436
|
return text;
|
|
@@ -1292,6 +1292,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
1292
1292
|
default: {} as Record<string, string>,
|
|
1293
1293
|
},
|
|
1294
1294
|
|
|
1295
|
+
"tasks.todoClearDelay": {
|
|
1296
|
+
type: "number",
|
|
1297
|
+
default: 60,
|
|
1298
|
+
ui: {
|
|
1299
|
+
tab: "tasks",
|
|
1300
|
+
label: "Todo auto-clear delay",
|
|
1301
|
+
description: "How long to wait before removing completed/abandoned tasks from the list",
|
|
1302
|
+
submenu: true,
|
|
1303
|
+
},
|
|
1304
|
+
},
|
|
1305
|
+
|
|
1295
1306
|
// Skills
|
|
1296
1307
|
"skills.enabled": { type: "boolean", default: true },
|
|
1297
1308
|
|
|
@@ -10,6 +10,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
10
10
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
11
11
|
import type { Rule } from "../../capability/rule";
|
|
12
12
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
13
|
+
import type { Settings } from "../../config/settings";
|
|
13
14
|
import type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
14
15
|
import type { HookUIContext } from "../../extensibility/hooks/types";
|
|
15
16
|
import type { Theme } from "../../modes/theme/theme";
|
|
@@ -76,6 +77,8 @@ export interface CustomToolContext {
|
|
|
76
77
|
hasQueuedMessages(): boolean;
|
|
77
78
|
/** Abort the current agent operation (fire-and-forget, does not wait) */
|
|
78
79
|
abort(): void;
|
|
80
|
+
/** Settings instance for the current session. Prefer over the global singleton. */
|
|
81
|
+
settings?: Settings;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
/** Session event passed to onSession callback */
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import { slashCommandCapability } from "../capability/slash-command";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
appendInlineArgsFallback,
|
|
5
|
+
renderPromptTemplate,
|
|
6
|
+
templateUsesInlineArgPlaceholders,
|
|
7
|
+
} from "../config/prompt-templates";
|
|
4
8
|
import type { SlashCommand } from "../discovery";
|
|
5
9
|
import { loadCapability } from "../discovery";
|
|
6
10
|
import {
|
|
@@ -217,8 +221,10 @@ export function expandSlashCommand(text: string, fileCommands: FileSlashCommand[
|
|
|
217
221
|
if (fileCommand) {
|
|
218
222
|
const args = parseCommandArgs(argsString);
|
|
219
223
|
const argsText = args.join(" ");
|
|
224
|
+
const usesInlineArgPlaceholders = templateUsesInlineArgPlaceholders(fileCommand.content);
|
|
220
225
|
const substituted = substituteArgs(fileCommand.content, args);
|
|
221
|
-
|
|
226
|
+
const rendered = renderPromptTemplate(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
|
|
227
|
+
return appendInlineArgsFallback(rendered, argsText, usesInlineArgPlaceholders);
|
|
222
228
|
}
|
|
223
229
|
|
|
224
230
|
return text;
|
|
@@ -248,6 +248,17 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
248
248
|
{ value: "1000", label: "1000 lines" },
|
|
249
249
|
{ value: "5000", label: "5000 lines" },
|
|
250
250
|
],
|
|
251
|
+
// Todo auto-clear delay
|
|
252
|
+
"tasks.todoClearDelay": [
|
|
253
|
+
{ value: "0", label: "Instant" },
|
|
254
|
+
{ value: "60", label: "1 minute", description: "Default" },
|
|
255
|
+
{ value: "300", label: "5 minutes" },
|
|
256
|
+
{ value: "900", label: "15 minutes" },
|
|
257
|
+
{ value: "1800", label: "30 minutes" },
|
|
258
|
+
{ value: "3600", label: "1 hour" },
|
|
259
|
+
{ value: "-1", label: "Never" },
|
|
260
|
+
],
|
|
261
|
+
|
|
251
262
|
// Edit fuzzy threshold
|
|
252
263
|
"edit.fuzzyThreshold": [
|
|
253
264
|
{ value: "0.85", label: "0.85", description: "Lenient" },
|
|
@@ -542,7 +542,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
542
542
|
|
|
543
543
|
const indent = " ";
|
|
544
544
|
const hook = theme.tree.hook;
|
|
545
|
-
const lines = [indent + theme.bold(theme.fg("accent", "Todos"))];
|
|
545
|
+
const lines = ["", indent + theme.bold(theme.fg("accent", "Todos"))];
|
|
546
546
|
|
|
547
547
|
if (!this.todoExpanded) {
|
|
548
548
|
const activePhase = this.#getActivePhase(phases);
|
package/src/sdk.ts
CHANGED
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
loadProjectContextFiles as loadContextFilesInternal,
|
|
80
80
|
} from "./system-prompt";
|
|
81
81
|
import { AgentOutputManager } from "./task/output-manager";
|
|
82
|
-
import { resolveThinkingLevelForModel, toReasoningEffort } from "./thinking";
|
|
82
|
+
import { parseThinkingLevel, resolveThinkingLevelForModel, toReasoningEffort } from "./thinking";
|
|
83
83
|
import {
|
|
84
84
|
BashTool,
|
|
85
85
|
BUILTIN_TOOLS,
|
|
@@ -626,7 +626,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
626
626
|
if (!options.modelRegistry) {
|
|
627
627
|
modelRegistry.refreshInBackground();
|
|
628
628
|
}
|
|
629
|
-
const skillsSettings = settings.getGroup("skills")
|
|
629
|
+
const skillsSettings = settings.getGroup("skills");
|
|
630
630
|
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
|
|
631
631
|
const discoveredSkillsPromise =
|
|
632
632
|
options.skills === undefined
|
|
@@ -711,7 +711,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
711
711
|
|
|
712
712
|
// If session has data and includes a thinking entry, restore it
|
|
713
713
|
if (thinkingLevel === undefined && hasExistingSession && hasThinkingEntry) {
|
|
714
|
-
thinkingLevel = existingSession.thinkingLevel
|
|
714
|
+
thinkingLevel = parseThinkingLevel(existingSession.thinkingLevel);
|
|
715
715
|
}
|
|
716
716
|
|
|
717
717
|
if (thinkingLevel === undefined && !hasExplicitModel && !hasThinkingEntry && defaultRoleSpec.explicitThinkingLevel) {
|
|
@@ -935,7 +935,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
935
935
|
// Always filter Exa - we have native integration
|
|
936
936
|
filterExa: true,
|
|
937
937
|
// Filter browser MCP servers when builtin browser tool is active
|
|
938
|
-
filterBrowser:
|
|
938
|
+
filterBrowser: settings.get("browser.enabled") ?? false,
|
|
939
939
|
cacheStorage: settings.getStorage(),
|
|
940
940
|
authStorage,
|
|
941
941
|
}),
|
|
@@ -1005,10 +1005,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1005
1005
|
extensionsResult = options.preloadedExtensions;
|
|
1006
1006
|
} else {
|
|
1007
1007
|
// Merge CLI extension paths with settings extension paths
|
|
1008
|
-
const configuredPaths = [
|
|
1009
|
-
...(options.additionalExtensionPaths ?? []),
|
|
1010
|
-
...((settings.get("extensions") as string[]) ?? []),
|
|
1011
|
-
];
|
|
1008
|
+
const configuredPaths = [...(options.additionalExtensionPaths ?? []), ...(settings.get("extensions") ?? [])];
|
|
1012
1009
|
const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
|
|
1013
1010
|
extensionsResult = await logger.timeAsync(
|
|
1014
1011
|
"discoverAndLoadExtensions",
|
|
@@ -1118,11 +1115,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1118
1115
|
abort: () => {
|
|
1119
1116
|
session.abort();
|
|
1120
1117
|
},
|
|
1118
|
+
settings,
|
|
1121
1119
|
});
|
|
1122
1120
|
const toolContextStore = new ToolContextStore(getSessionContext);
|
|
1123
1121
|
|
|
1124
1122
|
const registeredTools = extensionRunner?.getAllRegisteredTools() ?? [];
|
|
1125
|
-
let wrappedExtensionTools:
|
|
1123
|
+
let wrappedExtensionTools: Tool[];
|
|
1126
1124
|
|
|
1127
1125
|
if (extensionRunner) {
|
|
1128
1126
|
// With extension runner: convert CustomTools to ToolDefinitions and wrap all together
|
|
@@ -1144,16 +1142,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1144
1142
|
isIdle: () => !session?.isStreaming,
|
|
1145
1143
|
hasQueuedMessages: () => (session?.queuedMessageCount ?? 0) > 0,
|
|
1146
1144
|
abort: () => session?.abort(),
|
|
1145
|
+
settings,
|
|
1147
1146
|
});
|
|
1148
1147
|
wrappedExtensionTools = (options.customTools ?? [])
|
|
1149
1148
|
.filter(isCustomTool)
|
|
1150
|
-
.map(tool => CustomToolAdapter.wrap(tool, customToolContext)
|
|
1149
|
+
.map(tool => CustomToolAdapter.wrap(tool, customToolContext));
|
|
1151
1150
|
}
|
|
1152
1151
|
|
|
1153
1152
|
// All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
|
|
1154
|
-
const toolRegistry = new Map<string,
|
|
1153
|
+
const toolRegistry = new Map<string, Tool>();
|
|
1155
1154
|
for (const tool of builtinTools) {
|
|
1156
|
-
toolRegistry.set(tool.name, tool
|
|
1155
|
+
toolRegistry.set(tool.name, tool);
|
|
1157
1156
|
}
|
|
1158
1157
|
for (const tool of wrappedExtensionTools) {
|
|
1159
1158
|
toolRegistry.set(tool.name, tool);
|
|
@@ -1173,7 +1172,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1173
1172
|
} else if (!toolRegistry.has("resolve")) {
|
|
1174
1173
|
const resolveTool = await logger.timeAsync("createTools:resolve:session", HIDDEN_TOOLS.resolve, toolSession);
|
|
1175
1174
|
if (resolveTool) {
|
|
1176
|
-
toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool)
|
|
1175
|
+
toolRegistry.set(resolveTool.name, wrapToolWithMetaNotice(resolveTool));
|
|
1177
1176
|
}
|
|
1178
1177
|
}
|
|
1179
1178
|
|
|
@@ -1218,7 +1217,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1218
1217
|
tools,
|
|
1219
1218
|
toolNames,
|
|
1220
1219
|
rules: rulebookRules,
|
|
1221
|
-
skillsSettings: settings.getGroup("skills")
|
|
1220
|
+
skillsSettings: settings.getGroup("skills"),
|
|
1222
1221
|
appendSystemPrompt: appendPrompt,
|
|
1223
1222
|
repeatToolDescriptions,
|
|
1224
1223
|
eagerTasks,
|
|
@@ -1236,7 +1235,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1236
1235
|
tools,
|
|
1237
1236
|
toolNames,
|
|
1238
1237
|
rules: rulebookRules,
|
|
1239
|
-
skillsSettings: settings.getGroup("skills")
|
|
1238
|
+
skillsSettings: settings.getGroup("skills"),
|
|
1240
1239
|
customPrompt: options.systemPrompt,
|
|
1241
1240
|
appendSystemPrompt: appendPrompt,
|
|
1242
1241
|
repeatToolDescriptions,
|
|
@@ -1297,17 +1296,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1297
1296
|
if (hasImages) {
|
|
1298
1297
|
const filteredContent = content
|
|
1299
1298
|
.map(c => (c.type === "image" ? { type: "text" as const, text: "Image reading is disabled." } : c))
|
|
1300
|
-
.filter(
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
i > 0 &&
|
|
1307
|
-
arr[i - 1].type === "text" &&
|
|
1308
|
-
(arr[i - 1] as { type: "text"; text: string }).text === "Image reading is disabled."
|
|
1309
|
-
),
|
|
1310
|
-
);
|
|
1299
|
+
.filter((c, i, arr) => {
|
|
1300
|
+
// Dedupe consecutive "Image reading is disabled." texts
|
|
1301
|
+
if (!(c.type === "text" && c.text === "Image reading is disabled." && i > 0)) return true;
|
|
1302
|
+
const prev = arr[i - 1];
|
|
1303
|
+
return !(prev.type === "text" && prev.text === "Image reading is disabled.");
|
|
1304
|
+
});
|
|
1311
1305
|
return { ...msg, content: filteredContent };
|
|
1312
1306
|
}
|
|
1313
1307
|
}
|
|
@@ -1439,7 +1433,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1439
1433
|
customCommands: customCommandsResult.commands,
|
|
1440
1434
|
skills,
|
|
1441
1435
|
skillWarnings,
|
|
1442
|
-
skillsSettings: settings.getGroup("skills")
|
|
1436
|
+
skillsSettings: settings.getGroup("skills"),
|
|
1443
1437
|
modelRegistry,
|
|
1444
1438
|
toolRegistry,
|
|
1445
1439
|
transformContext,
|
|
@@ -1514,7 +1508,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1514
1508
|
mcpManager.setOnResourcesChanged((serverName, uri) => {
|
|
1515
1509
|
logger.debug("MCP resources changed", { path: `mcp:${serverName}`, uri });
|
|
1516
1510
|
if (!settings.get("mcp.notifications")) return;
|
|
1517
|
-
const debounceMs =
|
|
1511
|
+
const debounceMs = settings.get("mcp.notificationDebounceMs");
|
|
1518
1512
|
const key = `${serverName}:${uri}`;
|
|
1519
1513
|
const existing = notificationDebounceTimers.get(key);
|
|
1520
1514
|
if (existing) clearTimeout(existing);
|
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
Effort,
|
|
32
32
|
ImageContent,
|
|
33
33
|
Message,
|
|
34
|
+
MessageAttribution,
|
|
34
35
|
Model,
|
|
35
36
|
ProviderSessionState,
|
|
36
37
|
ServiceTier,
|
|
@@ -154,7 +155,8 @@ export type AgentSessionEvent =
|
|
|
154
155
|
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
155
156
|
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
156
157
|
| { type: "ttsr_triggered"; rules: Rule[] }
|
|
157
|
-
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
158
|
+
| { type: "todo_reminder"; todos: TodoItem[]; attempt: number; maxAttempts: number }
|
|
159
|
+
| { type: "todo_auto_clear" };
|
|
158
160
|
|
|
159
161
|
/** Listener function for agent session events */
|
|
160
162
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
@@ -191,7 +193,7 @@ export interface AgentSessionConfig {
|
|
|
191
193
|
skillWarnings?: SkillWarning[];
|
|
192
194
|
/** Custom commands (TypeScript slash commands) */
|
|
193
195
|
customCommands?: LoadedCustomCommand[];
|
|
194
|
-
skillsSettings?:
|
|
196
|
+
skillsSettings?: SkillsSettings;
|
|
195
197
|
/** Model registry for API key resolution and model discovery */
|
|
196
198
|
modelRegistry: ModelRegistry;
|
|
197
199
|
/** Tool registry for LSP and settings */
|
|
@@ -224,6 +226,8 @@ export interface PromptOptions {
|
|
|
224
226
|
toolChoice?: ToolChoice;
|
|
225
227
|
/** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
|
|
226
228
|
synthetic?: boolean;
|
|
229
|
+
/** Explicit billing/initiator attribution for the prompt. Defaults to user prompts as `user` and synthetic prompts as `agent`. */
|
|
230
|
+
attribution?: MessageAttribution;
|
|
227
231
|
/** Skip pre-send compaction checks for this prompt (internal use for maintenance flows). */
|
|
228
232
|
skipCompactionCheck?: boolean;
|
|
229
233
|
}
|
|
@@ -361,6 +365,7 @@ export class AgentSession {
|
|
|
361
365
|
// Todo completion reminder state
|
|
362
366
|
#todoReminderCount = 0;
|
|
363
367
|
#todoPhases: TodoPhase[] = [];
|
|
368
|
+
#todoClearTimers = new Map<string, Timer>();
|
|
364
369
|
#nextToolChoiceOverride: ToolChoice | undefined = undefined;
|
|
365
370
|
|
|
366
371
|
// Bash execution state
|
|
@@ -383,7 +388,7 @@ export class AgentSession {
|
|
|
383
388
|
/** MCP prompt commands (updated dynamically when prompts are loaded) */
|
|
384
389
|
#mcpPromptCommands: LoadedCustomCommand[] = [];
|
|
385
390
|
|
|
386
|
-
#skillsSettings:
|
|
391
|
+
#skillsSettings: SkillsSettings | undefined;
|
|
387
392
|
|
|
388
393
|
// Model registry for API key resolution
|
|
389
394
|
#modelRegistry: ModelRegistry;
|
|
@@ -1534,6 +1539,7 @@ export class AgentSession {
|
|
|
1534
1539
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
1535
1540
|
}
|
|
1536
1541
|
this.#cancelPostPromptTasks();
|
|
1542
|
+
this.#clearTodoClearTimers();
|
|
1537
1543
|
const drained = await this.#asyncJobManager?.dispose({ timeoutMs: 3_000 });
|
|
1538
1544
|
const deliveryState = this.#asyncJobManager?.getDeliveryState();
|
|
1539
1545
|
if (drained === false && deliveryState) {
|
|
@@ -1991,9 +1997,10 @@ export class AgentSession {
|
|
|
1991
1997
|
userContent.push(...options.images);
|
|
1992
1998
|
}
|
|
1993
1999
|
|
|
2000
|
+
const promptAttribution = options?.attribution ?? (options?.synthetic ? "agent" : "user");
|
|
1994
2001
|
const message = options?.synthetic
|
|
1995
|
-
? { role: "developer" as const, content: userContent, attribution:
|
|
1996
|
-
: { role: "user" as const, content: userContent, attribution:
|
|
2002
|
+
? { role: "developer" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() }
|
|
2003
|
+
: { role: "user" as const, content: userContent, attribution: promptAttribution, timestamp: Date.now() };
|
|
1997
2004
|
|
|
1998
2005
|
if (eagerTodoPrelude) {
|
|
1999
2006
|
this.#nextToolChoiceOverride = eagerTodoPrelude.toolChoice;
|
|
@@ -2518,7 +2525,7 @@ export class AgentSession {
|
|
|
2518
2525
|
return undefined;
|
|
2519
2526
|
}
|
|
2520
2527
|
|
|
2521
|
-
get skillsSettings():
|
|
2528
|
+
get skillsSettings(): SkillsSettings | undefined {
|
|
2522
2529
|
return this.#skillsSettings;
|
|
2523
2530
|
}
|
|
2524
2531
|
|
|
@@ -2538,10 +2545,17 @@ export class AgentSession {
|
|
|
2538
2545
|
|
|
2539
2546
|
setTodoPhases(phases: TodoPhase[]): void {
|
|
2540
2547
|
this.#todoPhases = this.#cloneTodoPhases(phases);
|
|
2548
|
+
this.#scheduleTodoAutoClear(phases);
|
|
2541
2549
|
}
|
|
2542
2550
|
|
|
2543
2551
|
#syncTodoPhasesFromBranch(): void {
|
|
2544
|
-
|
|
2552
|
+
const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getBranch());
|
|
2553
|
+
// Strip completed/abandoned tasks — they were done in a previous run,
|
|
2554
|
+
// so the auto-clear grace period has already elapsed.
|
|
2555
|
+
for (const phase of phases) {
|
|
2556
|
+
phase.tasks = phase.tasks.filter(t => t.status !== "completed" && t.status !== "abandoned");
|
|
2557
|
+
}
|
|
2558
|
+
this.setTodoPhases(phases.filter(p => p.tasks.length > 0));
|
|
2545
2559
|
}
|
|
2546
2560
|
|
|
2547
2561
|
#cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
|
|
@@ -2557,6 +2571,68 @@ export class AgentSession {
|
|
|
2557
2571
|
}));
|
|
2558
2572
|
}
|
|
2559
2573
|
|
|
2574
|
+
/** Schedule auto-removal of completed/abandoned tasks after a delay. */
|
|
2575
|
+
#scheduleTodoAutoClear(phases: TodoPhase[]): void {
|
|
2576
|
+
const delaySec = this.settings.get("tasks.todoClearDelay") ?? 60;
|
|
2577
|
+
if (delaySec < 0) return; // "Never" — no auto-clear
|
|
2578
|
+
const delayMs = delaySec * 1000;
|
|
2579
|
+
const doneTaskIds = new Set<string>();
|
|
2580
|
+
for (const phase of phases) {
|
|
2581
|
+
for (const task of phase.tasks) {
|
|
2582
|
+
if (task.status === "completed" || task.status === "abandoned") {
|
|
2583
|
+
doneTaskIds.add(task.id);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
// Cancel timers for tasks that are no longer done (e.g. status was reverted)
|
|
2589
|
+
for (const [id, timer] of this.#todoClearTimers) {
|
|
2590
|
+
if (!doneTaskIds.has(id)) {
|
|
2591
|
+
clearTimeout(timer);
|
|
2592
|
+
this.#todoClearTimers.delete(id);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
// Schedule new timers for newly-done tasks
|
|
2597
|
+
for (const id of doneTaskIds) {
|
|
2598
|
+
if (this.#todoClearTimers.has(id)) continue;
|
|
2599
|
+
if (delayMs === 0) {
|
|
2600
|
+
// Instant — run synchronously on next microtask to batch removals
|
|
2601
|
+
const timer = setTimeout(() => this.#runTodoAutoClear(id), 0);
|
|
2602
|
+
this.#todoClearTimers.set(id, timer);
|
|
2603
|
+
} else {
|
|
2604
|
+
const timer = setTimeout(() => this.#runTodoAutoClear(id), delayMs);
|
|
2605
|
+
this.#todoClearTimers.set(id, timer);
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
/** Remove a single completed task and notify the UI. */
|
|
2611
|
+
#runTodoAutoClear(taskId: string): void {
|
|
2612
|
+
this.#todoClearTimers.delete(taskId);
|
|
2613
|
+
let removed = false;
|
|
2614
|
+
for (const phase of this.#todoPhases) {
|
|
2615
|
+
const idx = phase.tasks.findIndex(t => t.id === taskId);
|
|
2616
|
+
if (idx !== -1 && (phase.tasks[idx].status === "completed" || phase.tasks[idx].status === "abandoned")) {
|
|
2617
|
+
phase.tasks.splice(idx, 1);
|
|
2618
|
+
removed = true;
|
|
2619
|
+
break;
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
if (!removed) return;
|
|
2623
|
+
|
|
2624
|
+
// Remove empty phases
|
|
2625
|
+
this.#todoPhases = this.#todoPhases.filter(p => p.tasks.length > 0);
|
|
2626
|
+
this.#emit({ type: "todo_auto_clear" });
|
|
2627
|
+
}
|
|
2628
|
+
|
|
2629
|
+
#clearTodoClearTimers(): void {
|
|
2630
|
+
for (const timer of this.#todoClearTimers.values()) {
|
|
2631
|
+
clearTimeout(timer);
|
|
2632
|
+
}
|
|
2633
|
+
this.#todoClearTimers.clear();
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2560
2636
|
/**
|
|
2561
2637
|
* Abort current operation and wait for agent to become idle.
|
|
2562
2638
|
*/
|
|
@@ -347,6 +347,44 @@ export function migrateSessionEntries(entries: FileEntry[]): void {
|
|
|
347
347
|
|
|
348
348
|
let sessionDirsMigrated = false;
|
|
349
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Merge or rename a legacy session directory into its canonical target.
|
|
352
|
+
* Best effort: callers decide whether migration failures should surface.
|
|
353
|
+
*/
|
|
354
|
+
function migrateSessionDirPath(oldPath: string, newPath: string): void {
|
|
355
|
+
const existing = fs.statSync(newPath, { throwIfNoEntry: false });
|
|
356
|
+
if (existing?.isDirectory()) {
|
|
357
|
+
for (const file of fs.readdirSync(oldPath)) {
|
|
358
|
+
const src = path.join(oldPath, file);
|
|
359
|
+
const dst = path.join(newPath, file);
|
|
360
|
+
if (!fs.existsSync(dst)) {
|
|
361
|
+
fs.renameSync(src, dst);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
fs.rmSync(oldPath, { recursive: true, force: true });
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (existing) {
|
|
368
|
+
fs.rmSync(newPath, { recursive: true, force: true });
|
|
369
|
+
}
|
|
370
|
+
fs.renameSync(oldPath, newPath);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
|
|
374
|
+
const resolvedCwd = path.resolve(cwd);
|
|
375
|
+
return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function pathIsWithin(root: string, candidate: string): boolean {
|
|
379
|
+
const relative = path.relative(root, candidate);
|
|
380
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function encodeRelativeSessionDirName(prefix: string, root: string, cwd: string): string {
|
|
384
|
+
const relative = path.relative(root, cwd).replace(/[/\\:]/g, "-");
|
|
385
|
+
return relative ? `${prefix}-${relative}` : prefix;
|
|
386
|
+
}
|
|
387
|
+
|
|
350
388
|
/**
|
|
351
389
|
* Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
|
|
352
390
|
* Runs once on first access, best-effort.
|
|
@@ -378,34 +416,29 @@ function migrateHomeSessionDirs(): void {
|
|
|
378
416
|
continue;
|
|
379
417
|
}
|
|
380
418
|
|
|
381
|
-
const newName = `-${remainder}
|
|
419
|
+
const newName = remainder ? `-${remainder}` : "-";
|
|
382
420
|
const oldPath = path.join(sessionsRoot, entry);
|
|
383
421
|
const newPath = path.join(sessionsRoot, newName);
|
|
384
422
|
|
|
385
423
|
try {
|
|
386
|
-
|
|
387
|
-
if (existing?.isDirectory()) {
|
|
388
|
-
// Merge files from old dir into existing new dir
|
|
389
|
-
for (const file of fs.readdirSync(oldPath)) {
|
|
390
|
-
const src = path.join(oldPath, file);
|
|
391
|
-
const dst = path.join(newPath, file);
|
|
392
|
-
if (!fs.existsSync(dst)) {
|
|
393
|
-
fs.renameSync(src, dst);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
fs.rmSync(oldPath, { recursive: true, force: true });
|
|
397
|
-
} else {
|
|
398
|
-
if (existing) {
|
|
399
|
-
fs.rmSync(newPath, { recursive: true, force: true });
|
|
400
|
-
}
|
|
401
|
-
fs.renameSync(oldPath, newPath);
|
|
402
|
-
}
|
|
424
|
+
migrateSessionDirPath(oldPath, newPath);
|
|
403
425
|
} catch {
|
|
404
426
|
// Best effort
|
|
405
427
|
}
|
|
406
428
|
}
|
|
407
429
|
}
|
|
408
430
|
|
|
431
|
+
function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string): void {
|
|
432
|
+
const legacyDir = path.join(getSessionsDir(), encodeLegacyAbsoluteSessionDirName(cwd));
|
|
433
|
+
if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return;
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
migrateSessionDirPath(legacyDir, sessionDir);
|
|
437
|
+
} catch {
|
|
438
|
+
// Best effort
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
409
442
|
/** Exported for compaction.test.ts */
|
|
410
443
|
export function parseSessionEntries(content: string): FileEntry[] {
|
|
411
444
|
return parseJsonlLenient<FileEntry>(content);
|
|
@@ -603,23 +636,31 @@ export function buildSessionContext(
|
|
|
603
636
|
/**
|
|
604
637
|
* Encode a cwd into a safe directory name for session storage.
|
|
605
638
|
* Home-relative paths use single-dash format: `/Users/x/Projects/pi` → `-Projects-pi`
|
|
606
|
-
*
|
|
639
|
+
* Temp-root paths use `-tmp-` prefixes: `/tmp/foo` → `-tmp-foo`
|
|
640
|
+
* Other absolute paths keep the legacy double-dash format for compatibility.
|
|
607
641
|
*/
|
|
608
642
|
function encodeSessionDirName(cwd: string): string {
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
return
|
|
643
|
+
const resolvedCwd = path.resolve(cwd);
|
|
644
|
+
const home = path.resolve(os.homedir());
|
|
645
|
+
if (pathIsWithin(home, resolvedCwd)) {
|
|
646
|
+
return encodeRelativeSessionDirName("-", home, resolvedCwd);
|
|
647
|
+
}
|
|
648
|
+
const tempRoot = path.resolve(os.tmpdir());
|
|
649
|
+
if (pathIsWithin(tempRoot, resolvedCwd)) {
|
|
650
|
+
return encodeRelativeSessionDirName("-tmp", tempRoot, resolvedCwd);
|
|
613
651
|
}
|
|
614
|
-
return
|
|
652
|
+
return encodeLegacyAbsoluteSessionDirName(resolvedCwd);
|
|
615
653
|
}
|
|
654
|
+
|
|
616
655
|
/**
|
|
617
656
|
* Compute the default session directory for a cwd.
|
|
618
657
|
* Encodes cwd into a safe directory name under ~/.omp/agent/sessions/.
|
|
619
658
|
*/
|
|
620
659
|
function getDefaultSessionDir(cwd: string, storage: SessionStorage): string {
|
|
660
|
+
const resolvedCwd = path.resolve(cwd);
|
|
621
661
|
migrateHomeSessionDirs();
|
|
622
|
-
const sessionDir = path.join(getSessionsDir(), encodeSessionDirName(
|
|
662
|
+
const sessionDir = path.join(getSessionsDir(), encodeSessionDirName(resolvedCwd));
|
|
663
|
+
migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir);
|
|
623
664
|
storage.ensureDirSync(sessionDir);
|
|
624
665
|
return sessionDir;
|
|
625
666
|
}
|
|
@@ -1307,7 +1348,7 @@ export class SessionManager {
|
|
|
1307
1348
|
#fileEntries: FileEntry[] = [];
|
|
1308
1349
|
#byId: Map<string, SessionEntry> = new Map();
|
|
1309
1350
|
#labelsById: Map<string, string> = new Map();
|
|
1310
|
-
#leafId
|
|
1351
|
+
#leafId: string | null = null;
|
|
1311
1352
|
#usageStatistics = {
|
|
1312
1353
|
input: 0,
|
|
1313
1354
|
output: 0,
|
|
@@ -1316,7 +1357,7 @@ export class SessionManager {
|
|
|
1316
1357
|
premiumRequests: 0,
|
|
1317
1358
|
cost: 0,
|
|
1318
1359
|
} satisfies UsageStatistics;
|
|
1319
|
-
#persistWriter
|
|
1360
|
+
#persistWriter: NdjsonFileWriter | undefined;
|
|
1320
1361
|
#persistWriterPath: string | undefined;
|
|
1321
1362
|
#persistChain: Promise<void> = Promise.resolve();
|
|
1322
1363
|
#persistError: Error | undefined;
|
|
@@ -1326,8 +1367,8 @@ export class SessionManager {
|
|
|
1326
1367
|
readonly #blobStore: BlobStore;
|
|
1327
1368
|
|
|
1328
1369
|
private constructor(
|
|
1329
|
-
private
|
|
1330
|
-
private
|
|
1370
|
+
private cwd: string,
|
|
1371
|
+
private sessionDir: string,
|
|
1331
1372
|
private readonly persist: boolean,
|
|
1332
1373
|
private readonly storage: SessionStorage,
|
|
1333
1374
|
) {
|
|
@@ -1429,7 +1470,7 @@ export class SessionManager {
|
|
|
1429
1470
|
this.#sessionName = newHeader.title;
|
|
1430
1471
|
|
|
1431
1472
|
// Replace the header in fileEntries
|
|
1432
|
-
const entries = this.#fileEntries.filter(e => e.type !== "session")
|
|
1473
|
+
const entries = this.#fileEntries.filter((e): e is SessionEntry => e.type !== "session");
|
|
1433
1474
|
this.#fileEntries = [newHeader, ...entries];
|
|
1434
1475
|
|
|
1435
1476
|
// Write the new session file
|
|
@@ -1506,9 +1547,9 @@ export class SessionManager {
|
|
|
1506
1547
|
this.#sessionFile = newSessionFile;
|
|
1507
1548
|
}
|
|
1508
1549
|
|
|
1509
|
-
// Update cwd and sessionDir
|
|
1510
|
-
|
|
1511
|
-
|
|
1550
|
+
// Update cwd and sessionDir after the move succeeds.
|
|
1551
|
+
this.cwd = resolvedCwd;
|
|
1552
|
+
this.sessionDir = newSessionDir;
|
|
1512
1553
|
|
|
1513
1554
|
// Update the session header in fileEntries
|
|
1514
1555
|
const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
package/src/task/executor.ts
CHANGED
|
@@ -1078,7 +1078,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1078
1078
|
}
|
|
1079
1079
|
});
|
|
1080
1080
|
|
|
1081
|
-
await session.prompt(task);
|
|
1081
|
+
await session.prompt(task, { attribution: "agent" });
|
|
1082
1082
|
await session.waitForIdle();
|
|
1083
1083
|
|
|
1084
1084
|
const reminderToolChoice = buildNamedToolChoice("submit_result", session.model);
|
|
@@ -1092,7 +1092,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1092
1092
|
maxRetries: MAX_SUBMIT_RESULT_RETRIES,
|
|
1093
1093
|
});
|
|
1094
1094
|
|
|
1095
|
-
await session.prompt(reminder,
|
|
1095
|
+
await session.prompt(reminder, {
|
|
1096
|
+
attribution: "agent",
|
|
1097
|
+
...(reminderToolChoice ? { toolChoice: reminderToolChoice } : {}),
|
|
1098
|
+
});
|
|
1096
1099
|
await session.waitForIdle();
|
|
1097
1100
|
} catch (err) {
|
|
1098
1101
|
logger.error("Subagent prompt failed", {
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
AgentToolUpdateCallback,
|
|
13
13
|
} from "@oh-my-pi/pi-agent-core";
|
|
14
14
|
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
15
|
-
import {
|
|
15
|
+
import { getDefault, type Settings } from "../config/settings";
|
|
16
16
|
import { formatGroupedDiagnosticMessages } from "../lsp/utils";
|
|
17
17
|
import type { Theme } from "../modes/theme/theme";
|
|
18
18
|
import { type OutputSummary, type TruncationResult, truncateTail } from "../session/streaming-output";
|
|
@@ -415,16 +415,17 @@ export function formatStyledTruncationWarning(meta: OutputMeta | undefined, them
|
|
|
415
415
|
* Append output notice to tool result content if meta is present.
|
|
416
416
|
*/
|
|
417
417
|
function appendOutputNotice(
|
|
418
|
-
content:
|
|
418
|
+
content: (TextContent | ImageContent)[],
|
|
419
419
|
meta: OutputMeta | undefined,
|
|
420
|
-
):
|
|
420
|
+
): (TextContent | ImageContent)[] {
|
|
421
421
|
const notice = formatOutputNotice(meta);
|
|
422
422
|
if (!notice) return content;
|
|
423
423
|
|
|
424
424
|
const result = [...content];
|
|
425
425
|
for (let i = result.length - 1; i >= 0; i--) {
|
|
426
|
-
|
|
427
|
-
|
|
426
|
+
const item = result[i];
|
|
427
|
+
if (item.type === "text") {
|
|
428
|
+
result[i] = { ...item, text: item.text + notice };
|
|
428
429
|
return result;
|
|
429
430
|
}
|
|
430
431
|
}
|
|
@@ -439,19 +440,16 @@ const kUnwrappedExecute = Symbol("OutputMeta.UnwrappedExecute");
|
|
|
439
440
|
// Centralized artifact spill for large tool results
|
|
440
441
|
// =============================================================================
|
|
441
442
|
|
|
442
|
-
/**
|
|
443
|
-
function
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
/** When spilling, keep at most this many lines of tail. */
|
|
453
|
-
function getArtifactTailLines(): number {
|
|
454
|
-
return settings.get("tools.artifactTailLines");
|
|
443
|
+
/** Resolved artifact spill config sourced from the session settings (or schema defaults). */
|
|
444
|
+
function getSpillConfig(s: Settings | undefined) {
|
|
445
|
+
const get = <P extends "tools.artifactSpillThreshold" | "tools.artifactTailBytes" | "tools.artifactTailLines">(
|
|
446
|
+
path: P,
|
|
447
|
+
) => s?.get(path) ?? getDefault(path);
|
|
448
|
+
return {
|
|
449
|
+
threshold: get("tools.artifactSpillThreshold") * 1024,
|
|
450
|
+
tailBytes: get("tools.artifactTailBytes") * 1024,
|
|
451
|
+
tailLines: get("tools.artifactTailLines"),
|
|
452
|
+
};
|
|
455
453
|
}
|
|
456
454
|
|
|
457
455
|
/**
|
|
@@ -467,9 +465,10 @@ async function spillLargeResultToArtifact(
|
|
|
467
465
|
): Promise<AgentToolResult> {
|
|
468
466
|
const sessionManager = context?.sessionManager;
|
|
469
467
|
if (!sessionManager) return result;
|
|
468
|
+
const { threshold, tailBytes, tailLines } = getSpillConfig(context?.settings);
|
|
470
469
|
|
|
471
470
|
// Skip if tool already saved an artifact
|
|
472
|
-
const existingMeta =
|
|
471
|
+
const existingMeta: OutputMeta | undefined = result.details?.meta;
|
|
473
472
|
if (existingMeta?.truncation?.artifactId) return result;
|
|
474
473
|
|
|
475
474
|
// Measure total text content
|
|
@@ -483,7 +482,7 @@ async function spillLargeResultToArtifact(
|
|
|
483
482
|
|
|
484
483
|
const fullText = textParts.length === 1 ? textParts[0] : textParts.join("\n");
|
|
485
484
|
const totalBytes = Buffer.byteLength(fullText, "utf-8");
|
|
486
|
-
if (totalBytes <=
|
|
485
|
+
if (totalBytes <= threshold) return result;
|
|
487
486
|
|
|
488
487
|
// Save full output as artifact
|
|
489
488
|
const artifactId = await sessionManager.saveArtifact(fullText, toolName);
|
|
@@ -491,8 +490,8 @@ async function spillLargeResultToArtifact(
|
|
|
491
490
|
|
|
492
491
|
// Truncate to tail
|
|
493
492
|
const truncated = truncateTail(fullText, {
|
|
494
|
-
maxBytes:
|
|
495
|
-
maxLines:
|
|
493
|
+
maxBytes: tailBytes,
|
|
494
|
+
maxLines: tailLines,
|
|
496
495
|
});
|
|
497
496
|
|
|
498
497
|
// Replace text blocks with single tail-truncated block, keep images
|
|
@@ -515,7 +514,7 @@ async function spillLargeResultToArtifact(
|
|
|
515
514
|
totalBytes: truncated.totalBytes,
|
|
516
515
|
outputLines,
|
|
517
516
|
outputBytes,
|
|
518
|
-
maxBytes:
|
|
517
|
+
maxBytes: tailBytes,
|
|
519
518
|
shownRange: { start: shownStart, end: truncated.totalLines },
|
|
520
519
|
artifactId,
|
|
521
520
|
};
|
|
@@ -547,11 +546,11 @@ async function wrappedExecute(
|
|
|
547
546
|
result = await spillLargeResultToArtifact(result, this.name, context);
|
|
548
547
|
|
|
549
548
|
// Append notices from meta
|
|
550
|
-
const meta =
|
|
549
|
+
const meta: OutputMeta | undefined = result.details?.meta;
|
|
551
550
|
if (meta) {
|
|
552
551
|
return {
|
|
553
552
|
...result,
|
|
554
|
-
content: appendOutputNotice(result.content, meta)
|
|
553
|
+
content: appendOutputNotice(result.content, meta),
|
|
555
554
|
};
|
|
556
555
|
}
|
|
557
556
|
return result;
|