@oh-my-pi/pi-coding-agent 15.9.1 → 15.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -2
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/cli/dry-balance-cli.d.ts +104 -0
- package/dist/types/commands/dry-balance.d.ts +31 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +13 -4
- package/dist/types/config/settings.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
- package/dist/types/hindsight/bank.d.ts +17 -9
- package/dist/types/hindsight/mental-models.d.ts +1 -1
- package/dist/types/hindsight/state.d.ts +9 -3
- package/dist/types/mcp/manager.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/error-banner.d.ts +11 -0
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/transcript-container.d.ts +4 -2
- package/dist/types/modes/components/user-message.d.ts +1 -1
- package/dist/types/modes/image-references.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/types.d.ts +7 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/auth-storage.d.ts +2 -2
- package/dist/types/session/blob-store.d.ts +12 -11
- package/dist/types/session/session-manager.d.ts +5 -3
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +16 -1
- package/dist/types/tool-discovery/mode.d.ts +8 -0
- package/dist/types/tools/archive-reader.d.ts +5 -1
- package/dist/types/tools/index.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +11 -0
- package/dist/types/tui/hyperlink.d.ts +12 -0
- package/dist/types/web/search/render.d.ts +1 -2
- package/package.json +9 -9
- package/src/cli/classify-install-target.ts +31 -5
- package/src/cli/dry-balance-cli.ts +823 -0
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/cli-commands.ts +1 -0
- package/src/commands/dry-balance.ts +43 -0
- package/src/config/model-registry.ts +60 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +14 -4
- package/src/config/settings.ts +38 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
- package/src/discovery/github.ts +37 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/eval/__tests__/agent-bridge.test.ts +72 -0
- package/src/eval/py/tool-bridge.ts +43 -5
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
- package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
- package/src/hindsight/backend.ts +184 -35
- package/src/hindsight/bank.ts +32 -22
- package/src/hindsight/mental-models.ts +1 -1
- package/src/hindsight/state.ts +21 -7
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/omp-protocol.ts +8 -2
- package/src/main.ts +7 -1
- package/src/mcp/manager.ts +40 -21
- package/src/modes/components/assistant-message.ts +22 -0
- package/src/modes/components/custom-editor.ts +14 -2
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/tool-execution.ts +44 -0
- package/src/modes/components/transcript-container.ts +102 -30
- package/src/modes/components/tree-selector.ts +29 -2
- package/src/modes/components/user-message.ts +9 -2
- package/src/modes/controllers/event-controller.ts +42 -3
- package/src/modes/controllers/input-controller.ts +41 -3
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +48 -13
- package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
- package/src/modes/types.ts +10 -1
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/librarian.md +1 -0
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/sdk.ts +99 -18
- package/src/session/agent-session.ts +103 -19
- package/src/session/auth-storage.ts +4 -0
- package/src/session/blob-store.ts +96 -9
- package/src/session/session-manager.ts +19 -10
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +6 -2
- package/src/task/index.ts +8 -7
- package/src/task/types.ts +2 -0
- package/src/tiny/title-client.ts +7 -1
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tools/archive-reader.ts +339 -31
- package/src/tools/bash.ts +3 -4
- package/src/tools/fetch.ts +29 -9
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +22 -8
- package/src/tools/job.ts +3 -3
- package/src/tools/memory-reflect.ts +2 -2
- package/src/tools/path-utils.ts +21 -0
- package/src/tools/read.ts +58 -12
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +78 -12
- package/src/tui/hyperlink.ts +42 -7
- package/src/utils/file-mentions.ts +7 -107
- package/src/utils/title-generator.ts +58 -37
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +20 -52
package/src/sdk.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
extractRetryHint,
|
|
28
28
|
getAgentDbPath,
|
|
29
29
|
getAgentDir,
|
|
30
|
+
getAuthBrokerSnapshotCachePath,
|
|
30
31
|
getProjectDir,
|
|
31
32
|
logger,
|
|
32
33
|
postmortem,
|
|
@@ -101,7 +102,15 @@ import {
|
|
|
101
102
|
} from "./secrets";
|
|
102
103
|
import { AgentSession } from "./session/agent-session";
|
|
103
104
|
import { resolveAuthBrokerConfig } from "./session/auth-broker-config";
|
|
104
|
-
import {
|
|
105
|
+
import {
|
|
106
|
+
AuthBrokerClient,
|
|
107
|
+
AuthStorage,
|
|
108
|
+
DEFAULT_SNAPSHOT_CACHE_TTL_MS,
|
|
109
|
+
RemoteAuthCredentialStore,
|
|
110
|
+
readAuthBrokerSnapshotCache,
|
|
111
|
+
type SnapshotResponse,
|
|
112
|
+
writeAuthBrokerSnapshotCache,
|
|
113
|
+
} from "./session/auth-storage";
|
|
105
114
|
import { type CustomMessage, convertToLlm, wrapSteeringForModel } from "./session/messages";
|
|
106
115
|
import { getRestorableSessionModels, SessionManager } from "./session/session-manager";
|
|
107
116
|
import { closeAllConnections } from "./ssh/connection-manager";
|
|
@@ -121,6 +130,7 @@ import {
|
|
|
121
130
|
resolveThinkingLevelForModel,
|
|
122
131
|
toReasoningEffort,
|
|
123
132
|
} from "./thinking";
|
|
133
|
+
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "./tool-discovery/mode";
|
|
124
134
|
import {
|
|
125
135
|
collectDiscoverableTools,
|
|
126
136
|
type DiscoverableTool,
|
|
@@ -148,6 +158,7 @@ import {
|
|
|
148
158
|
ResolveTool,
|
|
149
159
|
renderSearchToolBm25Description,
|
|
150
160
|
SearchTool,
|
|
161
|
+
SearchToolBm25Tool,
|
|
151
162
|
setPreferredImageProvider,
|
|
152
163
|
setPreferredSearchProvider,
|
|
153
164
|
type Tool,
|
|
@@ -418,6 +429,17 @@ function getDefaultAgentDir(): string {
|
|
|
418
429
|
return getAgentDir();
|
|
419
430
|
}
|
|
420
431
|
|
|
432
|
+
function resolveSnapshotTtlMs(): number {
|
|
433
|
+
const raw = process.env.OMP_AUTH_BROKER_SNAPSHOT_TTL_MS;
|
|
434
|
+
if (raw === undefined) return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
435
|
+
const value = raw.trim();
|
|
436
|
+
if (value === "") return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
437
|
+
const ttlMs = Number(value);
|
|
438
|
+
if (Number.isFinite(ttlMs) && ttlMs >= 0) return ttlMs;
|
|
439
|
+
logger.warn("Invalid OMP_AUTH_BROKER_SNAPSHOT_TTL_MS; using default", { value: raw });
|
|
440
|
+
return DEFAULT_SNAPSHOT_CACHE_TTL_MS;
|
|
441
|
+
}
|
|
442
|
+
|
|
421
443
|
// Discovery Functions
|
|
422
444
|
|
|
423
445
|
/**
|
|
@@ -435,9 +457,42 @@ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir(
|
|
|
435
457
|
const brokerConfig = await resolveAuthBrokerConfig();
|
|
436
458
|
if (brokerConfig) {
|
|
437
459
|
const client = new AuthBrokerClient({ url: brokerConfig.url, token: brokerConfig.token });
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
const
|
|
460
|
+
const ttlMs = resolveSnapshotTtlMs();
|
|
461
|
+
const cachePath = getAuthBrokerSnapshotCachePath();
|
|
462
|
+
const persist =
|
|
463
|
+
ttlMs > 0
|
|
464
|
+
? (snapshot: SnapshotResponse): void => {
|
|
465
|
+
void writeAuthBrokerSnapshotCache({
|
|
466
|
+
path: cachePath,
|
|
467
|
+
token: brokerConfig.token,
|
|
468
|
+
url: brokerConfig.url,
|
|
469
|
+
snapshot,
|
|
470
|
+
}).catch(error => {
|
|
471
|
+
logger.debug("auth-broker snapshot cache write failed", { error: String(error) });
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
: undefined;
|
|
475
|
+
|
|
476
|
+
let initialSnapshot: SnapshotResponse | undefined;
|
|
477
|
+
if (ttlMs > 0) {
|
|
478
|
+
initialSnapshot =
|
|
479
|
+
(await readAuthBrokerSnapshotCache({
|
|
480
|
+
path: cachePath,
|
|
481
|
+
token: brokerConfig.token,
|
|
482
|
+
url: brokerConfig.url,
|
|
483
|
+
ttlMs,
|
|
484
|
+
}).catch(error => {
|
|
485
|
+
logger.debug("auth-broker snapshot cache read failed", { error: String(error) });
|
|
486
|
+
return null;
|
|
487
|
+
})) ?? undefined;
|
|
488
|
+
}
|
|
489
|
+
if (!initialSnapshot) {
|
|
490
|
+
const initialResult = await client.fetchSnapshot();
|
|
491
|
+
if (initialResult.status !== 200) throw new Error("Auth broker returned no initial snapshot");
|
|
492
|
+
initialSnapshot = initialResult.snapshot;
|
|
493
|
+
persist?.(initialSnapshot);
|
|
494
|
+
}
|
|
495
|
+
const store = new RemoteAuthCredentialStore({ client, initialSnapshot, onSnapshot: persist });
|
|
441
496
|
// Refresh + usage hooks live on RemoteAuthCredentialStore; AuthStorage
|
|
442
497
|
// discovers them automatically when no explicit option overrides them.
|
|
443
498
|
const storage = new AuthStorage(store, {
|
|
@@ -1168,12 +1223,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1168
1223
|
|
|
1169
1224
|
return preview;
|
|
1170
1225
|
};
|
|
1171
|
-
// Only top-level
|
|
1172
|
-
// parent's manager via `AsyncJobManager.instance()`
|
|
1173
|
-
//
|
|
1174
|
-
//
|
|
1226
|
+
// Only the first top-level session in a process owns an AsyncJobManager.
|
|
1227
|
+
// Subagents inherit the parent's manager via `AsyncJobManager.instance()`
|
|
1228
|
+
// (set below), and any additional top-level session spun up in-process
|
|
1229
|
+
// (e.g. the agent-creation architect in `agent-dashboard.ts`) must share
|
|
1230
|
+
// the live singleton — otherwise its dispose path would clobber the
|
|
1231
|
+
// owning session's manager and break the `task`/`bash` async paths
|
|
1232
|
+
// (issue #1923). The `instance()` guard means later sessions also skip
|
|
1233
|
+
// constructing an orphaned manager that nothing would ever route to.
|
|
1175
1234
|
const asyncJobManager =
|
|
1176
|
-
backgroundJobsEnabled && !options.parentTaskPrefix
|
|
1235
|
+
backgroundJobsEnabled && !options.parentTaskPrefix && !AsyncJobManager.instance()
|
|
1177
1236
|
? new AsyncJobManager({
|
|
1178
1237
|
maxRunningJobs: asyncMaxJobs,
|
|
1179
1238
|
onJobComplete: async (jobId, result, job) => {
|
|
@@ -1192,6 +1251,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1192
1251
|
})
|
|
1193
1252
|
: undefined;
|
|
1194
1253
|
|
|
1254
|
+
const scopedAsyncJobManager = asyncJobManager ?? (options.parentTaskPrefix ? AsyncJobManager.instance() : undefined);
|
|
1255
|
+
|
|
1195
1256
|
const agentRegistry = options.agentRegistry ?? AgentRegistry.global();
|
|
1196
1257
|
const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
|
|
1197
1258
|
const resolvedAgentDisplayName =
|
|
@@ -1293,6 +1354,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1293
1354
|
authStorage,
|
|
1294
1355
|
modelRegistry,
|
|
1295
1356
|
getTelemetry: () => agent?.telemetry,
|
|
1357
|
+
// Subagents inherit the singleton (the parent's manager) so their bash/task
|
|
1358
|
+
// completions still flow into the spawning conversation's yieldQueue.
|
|
1359
|
+
// Secondary in-process top-level sessions (no parentTaskPrefix, no
|
|
1360
|
+
// constructed manager because the singleton was already installed) leave
|
|
1361
|
+
// this undefined so tools and session job snapshots refuse async work
|
|
1362
|
+
// instead of silently routing into the owning session (issue #1923).
|
|
1363
|
+
asyncJobManager: scopedAsyncJobManager,
|
|
1296
1364
|
};
|
|
1297
1365
|
|
|
1298
1366
|
// Wire process-wide internal URL singletons owned by their real classes.
|
|
@@ -1621,6 +1689,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1621
1689
|
}
|
|
1622
1690
|
}
|
|
1623
1691
|
|
|
1692
|
+
const effectiveDiscoveryMode = resolveEffectiveToolDiscoveryMode(
|
|
1693
|
+
settings,
|
|
1694
|
+
countToolsForAutoDiscovery(toolRegistry.keys()),
|
|
1695
|
+
);
|
|
1696
|
+
if (effectiveDiscoveryMode !== "off" && !toolRegistry.has("search_tool_bm25")) {
|
|
1697
|
+
const searchTool: Tool = new SearchToolBm25Tool(toolSession);
|
|
1698
|
+
toolRegistry.set(
|
|
1699
|
+
searchTool.name,
|
|
1700
|
+
new ExtensionToolWrapper(wrapToolWithMetaNotice(searchTool), extensionRunner) as Tool,
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
1704
|
+
|
|
1624
1705
|
const reloadSshTool = async (): Promise<AgentTool | null> => {
|
|
1625
1706
|
if (!requestedToolNameSet.has("ssh")) return null;
|
|
1626
1707
|
const sshTool = (await loadSshTool({
|
|
@@ -1707,6 +1788,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1707
1788
|
secretsEnabled,
|
|
1708
1789
|
workspaceTree: workspaceTreePromise,
|
|
1709
1790
|
memoryRootEnabled: memoryBackend.id === "local",
|
|
1791
|
+
model: settings.get("includeModelInPrompt") ? getActiveModelString() : undefined,
|
|
1710
1792
|
});
|
|
1711
1793
|
|
|
1712
1794
|
if (options.systemPrompt === undefined) {
|
|
@@ -1739,15 +1821,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1739
1821
|
const requestedToolNames = explicitlyRequestedToolNames ?? toolNamesFromRegistry;
|
|
1740
1822
|
const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
|
|
1741
1823
|
const requestedToolNameSet = new Set(normalizedRequested);
|
|
1742
|
-
// Effective discovery mode
|
|
1743
|
-
const toolsDiscoveryModeSetting = settings.get("tools.discoveryMode");
|
|
1744
|
-
const effectiveDiscoveryMode: "off" | "mcp-only" | "all" =
|
|
1745
|
-
toolsDiscoveryModeSetting !== "off"
|
|
1746
|
-
? (toolsDiscoveryModeSetting as "off" | "mcp-only" | "all")
|
|
1747
|
-
: settings.get("mcp.discoveryMode")
|
|
1748
|
-
? "mcp-only"
|
|
1749
|
-
: "off";
|
|
1750
|
-
const mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
1824
|
+
// Effective discovery mode is resolved after the full registry exists so auto mode can count MCP/extension tools.
|
|
1751
1825
|
const defaultInactiveToolNames = new Set(
|
|
1752
1826
|
registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
|
|
1753
1827
|
);
|
|
@@ -2049,6 +2123,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2049
2123
|
// AsyncJobManager on teardown; subagents inherit the parent's and
|
|
2050
2124
|
// **MUST NOT** tear it down.
|
|
2051
2125
|
ownedAsyncJobManager: asyncJobManager,
|
|
2126
|
+
asyncJobManager: scopedAsyncJobManager,
|
|
2052
2127
|
scopedModels: options.scopedModels,
|
|
2053
2128
|
promptTemplates,
|
|
2054
2129
|
slashCommands,
|
|
@@ -2262,6 +2337,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
2262
2337
|
await session.dispose();
|
|
2263
2338
|
} else {
|
|
2264
2339
|
if (hasRegistered) agentRegistry.unregister(resolvedAgentId);
|
|
2340
|
+
if (asyncJobManager) {
|
|
2341
|
+
if (AsyncJobManager.instance() === asyncJobManager) {
|
|
2342
|
+
AsyncJobManager.setInstance(undefined);
|
|
2343
|
+
}
|
|
2344
|
+
await asyncJobManager.dispose({ timeoutMs: 3_000 });
|
|
2345
|
+
}
|
|
2265
2346
|
await disposeKernelSessionsByOwner(evalKernelOwnerId);
|
|
2266
2347
|
}
|
|
2267
2348
|
} catch (cleanupError) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import * as crypto from "node:crypto";
|
|
17
17
|
import * as fs from "node:fs";
|
|
18
|
+
import * as os from "node:os";
|
|
18
19
|
import * as path from "node:path";
|
|
19
20
|
import { scheduler } from "node:timers/promises";
|
|
20
21
|
import { isPromise } from "node:util/types";
|
|
@@ -94,6 +95,7 @@ import {
|
|
|
94
95
|
isUnexpectedSocketCloseMessage,
|
|
95
96
|
logger,
|
|
96
97
|
prompt,
|
|
98
|
+
relativePathWithinRoot,
|
|
97
99
|
Snowflake,
|
|
98
100
|
} from "@oh-my-pi/pi-utils";
|
|
99
101
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
@@ -190,6 +192,7 @@ import {
|
|
|
190
192
|
toReasoningEffort,
|
|
191
193
|
} from "../thinking";
|
|
192
194
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
195
|
+
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
193
196
|
import {
|
|
194
197
|
buildDiscoverableToolSearchIndex,
|
|
195
198
|
collectDiscoverableTools,
|
|
@@ -366,6 +369,15 @@ export interface AgentSessionConfig {
|
|
|
366
369
|
* **MUST NOT** dispose it on their own teardown.
|
|
367
370
|
*/
|
|
368
371
|
ownedAsyncJobManager?: AsyncJobManager;
|
|
372
|
+
/**
|
|
373
|
+
* AsyncJobManager reachable by this session for scoped job actions.
|
|
374
|
+
*
|
|
375
|
+
* Top-level owners receive their own manager, subagents receive the inherited
|
|
376
|
+
* parent manager, and secondary in-process top-level sessions receive
|
|
377
|
+
* `undefined` so job snapshots and ACP drains cannot observe the primary's
|
|
378
|
+
* state.
|
|
379
|
+
*/
|
|
380
|
+
asyncJobManager?: AsyncJobManager;
|
|
369
381
|
/** Agent identity (registry id like "Main" or "Alice") used for IRC routing. */
|
|
370
382
|
agentId?: string;
|
|
371
383
|
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
|
@@ -890,6 +902,14 @@ export class AgentSession {
|
|
|
890
902
|
* this undefined and **MUST NOT** dispose the global instance on teardown.
|
|
891
903
|
*/
|
|
892
904
|
readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
|
|
905
|
+
/**
|
|
906
|
+
* AsyncJobManager scoped to this session for introspection/cancellation.
|
|
907
|
+
*
|
|
908
|
+
* This differs from `#ownedAsyncJobManager`: subagents can inherit a parent
|
|
909
|
+
* manager for their own owner id, while secondary top-level sessions are left
|
|
910
|
+
* undefined to avoid reading the primary's jobs.
|
|
911
|
+
*/
|
|
912
|
+
readonly #asyncJobManager: AsyncJobManager | undefined;
|
|
893
913
|
#pendingPythonMessages: PythonExecutionMessage[] = [];
|
|
894
914
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
895
915
|
#evalExecutionDisposing = false;
|
|
@@ -941,6 +961,13 @@ export class AgentSession {
|
|
|
941
961
|
* the dominant cause of prompt-cache invalidation in long sessions.
|
|
942
962
|
*/
|
|
943
963
|
#lastAppliedToolSignature: string | undefined;
|
|
964
|
+
/**
|
|
965
|
+
* Model identifier (`provider/id`) currently rendered into `#baseSystemPrompt`.
|
|
966
|
+
* The prompt surfaces the active model to the agent, so a model switch must
|
|
967
|
+
* trigger a rebuild. Compared against the live model after every model change
|
|
968
|
+
* to decide whether the cached prompt is stale.
|
|
969
|
+
*/
|
|
970
|
+
#promptModelKey: string | undefined;
|
|
944
971
|
#mcpDiscoveryEnabled = false;
|
|
945
972
|
#discoverableMCPTools = new Map<string, DiscoverableTool>();
|
|
946
973
|
#selectedMCPToolNames = new Set<string>();
|
|
@@ -1080,6 +1107,7 @@ export class AgentSession {
|
|
|
1080
1107
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
1081
1108
|
this.#parentEvalSessionId = config.parentEvalSessionId;
|
|
1082
1109
|
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
1110
|
+
this.#asyncJobManager = config.asyncJobManager ?? config.ownedAsyncJobManager;
|
|
1083
1111
|
this.#scopedModels = config.scopedModels ?? [];
|
|
1084
1112
|
if (config.thinkingLevel === AUTO_THINKING) {
|
|
1085
1113
|
// `auto` is session-level: keep the flag and show a provisional concrete
|
|
@@ -1153,6 +1181,7 @@ export class AgentSession {
|
|
|
1153
1181
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
1154
1182
|
this.#reloadSshTool = config.reloadSshTool;
|
|
1155
1183
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
1184
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
1156
1185
|
this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
|
|
1157
1186
|
this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
|
|
1158
1187
|
this.#selectedMCPToolNames = new Set(config.initialSelectedMCPToolNames ?? []);
|
|
@@ -1373,7 +1402,7 @@ export class AgentSession {
|
|
|
1373
1402
|
}
|
|
1374
1403
|
|
|
1375
1404
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
1376
|
-
const manager =
|
|
1405
|
+
const manager = this.#asyncJobManager;
|
|
1377
1406
|
if (!manager) return null;
|
|
1378
1407
|
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
1379
1408
|
const running = manager.getRunningJobs(ownerFilter).map(job => ({
|
|
@@ -1398,11 +1427,20 @@ export class AgentSession {
|
|
|
1398
1427
|
* Cancel async jobs registered by *this* agent only. Used by lifecycle
|
|
1399
1428
|
* transitions (newSession, switchSession, handoff, dispose) so a subagent
|
|
1400
1429
|
* cleans up its own background work without touching its parent's jobs.
|
|
1401
|
-
*
|
|
1430
|
+
*
|
|
1431
|
+
* Cancellation runs against this session's scoped manager. Subagents have
|
|
1432
|
+
* unique agent ids and inherit the parent's manager to clean up their own
|
|
1433
|
+
* jobs. A secondary in-process top-level session gets no scoped manager,
|
|
1434
|
+
* because it defaults to `MAIN_AGENT_ID`; reaching through the global
|
|
1435
|
+
* singleton would tear down the owning primary session's bash/task jobs at
|
|
1436
|
+
* dispose time (issue #1923).
|
|
1437
|
+
*
|
|
1438
|
+
* No-op when no manager is reachable or this session has no agent id.
|
|
1402
1439
|
*/
|
|
1403
1440
|
#cancelOwnAsyncJobs(): void {
|
|
1404
1441
|
if (!this.#agentId) return;
|
|
1405
|
-
|
|
1442
|
+
const manager = this.#asyncJobManager;
|
|
1443
|
+
manager?.cancelAll({ ownerId: this.#agentId });
|
|
1406
1444
|
}
|
|
1407
1445
|
|
|
1408
1446
|
// =========================================================================
|
|
@@ -2128,12 +2166,31 @@ export class AgentSession {
|
|
|
2128
2166
|
if (this.#pendingTtsrInjections.length === 0) return undefined;
|
|
2129
2167
|
const rules = this.#pendingTtsrInjections;
|
|
2130
2168
|
const content = rules
|
|
2131
|
-
.map(r =>
|
|
2169
|
+
.map(r =>
|
|
2170
|
+
prompt.render(ttsrInterruptTemplate, {
|
|
2171
|
+
name: r.name,
|
|
2172
|
+
path: this.#displayRulePath(r.path),
|
|
2173
|
+
content: r.content,
|
|
2174
|
+
}),
|
|
2175
|
+
)
|
|
2132
2176
|
.join("\n\n");
|
|
2133
2177
|
this.#pendingTtsrInjections = [];
|
|
2134
2178
|
return { content, rules };
|
|
2135
2179
|
}
|
|
2136
2180
|
|
|
2181
|
+
/**
|
|
2182
|
+
* Render a rule's file path for model-facing TTSR injections without leaking
|
|
2183
|
+
* the absolute home directory: cwd-relative when the rule lives in the
|
|
2184
|
+
* project, `~`-relative when it lives under home, else the raw path.
|
|
2185
|
+
*/
|
|
2186
|
+
#displayRulePath(rulePath: string): string {
|
|
2187
|
+
const cwdRel = relativePathWithinRoot(this.sessionManager.getCwd(), rulePath);
|
|
2188
|
+
if (cwdRel) return cwdRel;
|
|
2189
|
+
const homeRel = relativePathWithinRoot(os.homedir(), rulePath);
|
|
2190
|
+
if (homeRel) return `~/${homeRel}`;
|
|
2191
|
+
return rulePath;
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2137
2194
|
#addPendingTtsrInjections(rules: Rule[]): void {
|
|
2138
2195
|
const seen = new Set(this.#pendingTtsrInjections.map(rule => rule.name));
|
|
2139
2196
|
for (const rule of rules) {
|
|
@@ -2186,7 +2243,13 @@ export class AgentSession {
|
|
|
2186
2243
|
if (!rules || rules.length === 0) return undefined;
|
|
2187
2244
|
this.#perToolTtsrInjections.delete(ctx.toolCall.id);
|
|
2188
2245
|
const reminder = rules
|
|
2189
|
-
.map(r =>
|
|
2246
|
+
.map(r =>
|
|
2247
|
+
prompt.render(ttsrToolReminderTemplate, {
|
|
2248
|
+
name: r.name,
|
|
2249
|
+
path: this.#displayRulePath(r.path),
|
|
2250
|
+
content: r.content,
|
|
2251
|
+
}),
|
|
2252
|
+
)
|
|
2190
2253
|
.join("\n\n");
|
|
2191
2254
|
// The TTSR manager was already claimed at bucket time; only persistence remains.
|
|
2192
2255
|
const ruleNames = rules.map(r => r.name.trim()).filter(n => n.length > 0);
|
|
@@ -2990,8 +3053,13 @@ export class AgentSession {
|
|
|
2990
3053
|
this.#releasePowerAssertion();
|
|
2991
3054
|
await this.sessionManager.close();
|
|
2992
3055
|
this.#closeAllProviderSessions("dispose");
|
|
2993
|
-
|
|
3056
|
+
// Flush the retain queue BEFORE clearing the session's pointer so
|
|
3057
|
+
// `HindsightRetainQueue.#doFlush` still sees `session.getHindsightSessionState() === state`.
|
|
3058
|
+
// Reversed, the spliced batch survives just long enough to fail the
|
|
3059
|
+
// identity check and get dropped with a `session vanished` warning.
|
|
3060
|
+
const hindsightState = this.getHindsightSessionState();
|
|
2994
3061
|
await hindsightState?.flushRetainQueue();
|
|
3062
|
+
this.setHindsightSessionState(undefined);
|
|
2995
3063
|
hindsightState?.dispose();
|
|
2996
3064
|
const mnemopiState = setMnemopiSessionState(this, undefined);
|
|
2997
3065
|
mnemopiState?.dispose();
|
|
@@ -3069,7 +3137,7 @@ export class AgentSession {
|
|
|
3069
3137
|
}
|
|
3070
3138
|
|
|
3071
3139
|
async drainAsyncJobDeliveriesForAcp(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
3072
|
-
const manager =
|
|
3140
|
+
const manager = this.#asyncJobManager;
|
|
3073
3141
|
if (!manager) return false;
|
|
3074
3142
|
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
3075
3143
|
const before = manager.getDeliveryState(ownerFilter);
|
|
@@ -3205,9 +3273,21 @@ export class AgentSession {
|
|
|
3205
3273
|
return resolveEditMode(this.#getEditModeSession());
|
|
3206
3274
|
}
|
|
3207
3275
|
|
|
3208
|
-
|
|
3276
|
+
/**
|
|
3277
|
+
* Model key (`provider/id`) currently surfaced in the system prompt, or
|
|
3278
|
+
* undefined when the model is unset or `includeModelInPrompt` is disabled.
|
|
3279
|
+
*/
|
|
3280
|
+
#currentPromptModelKey(): string | undefined {
|
|
3281
|
+
if (!this.settings.get("includeModelInPrompt")) return undefined;
|
|
3282
|
+
return this.model ? formatModelString(this.model) : undefined;
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
async #syncAfterModelChange(previousEditMode: EditMode): Promise<void> {
|
|
3209
3286
|
const currentEditMode = this.#resolveActiveEditMode();
|
|
3210
|
-
|
|
3287
|
+
const editModeChanged = previousEditMode !== currentEditMode && this.getActiveToolNames().includes("edit");
|
|
3288
|
+
// The system prompt may surface the active model; a switch makes the cached prompt stale.
|
|
3289
|
+
const modelChanged = this.#currentPromptModelKey() !== this.#promptModelKey;
|
|
3290
|
+
if (editModeChanged || modelChanged) {
|
|
3211
3291
|
await this.refreshBaseSystemPrompt();
|
|
3212
3292
|
}
|
|
3213
3293
|
}
|
|
@@ -3246,12 +3326,14 @@ export class AgentSession {
|
|
|
3246
3326
|
|
|
3247
3327
|
// ── Generic tool discovery (covers built-in + MCP + extension) ────────────
|
|
3248
3328
|
|
|
3249
|
-
/** Resolve effective discovery mode
|
|
3329
|
+
/** Resolve effective discovery mode from the current registry size. */
|
|
3250
3330
|
#resolveEffectiveDiscoveryMode(): "off" | "mcp-only" | "all" {
|
|
3251
|
-
const
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3331
|
+
const mode = resolveEffectiveToolDiscoveryMode(
|
|
3332
|
+
this.settings,
|
|
3333
|
+
countToolsForAutoDiscovery(this.#toolRegistry.keys()),
|
|
3334
|
+
);
|
|
3335
|
+
if (mode !== "off") return mode;
|
|
3336
|
+
return this.#mcpDiscoveryEnabled ? "mcp-only" : "off";
|
|
3255
3337
|
}
|
|
3256
3338
|
|
|
3257
3339
|
isToolDiscoveryEnabled(): boolean {
|
|
@@ -3492,6 +3574,7 @@ export class AgentSession {
|
|
|
3492
3574
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
3493
3575
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
3494
3576
|
this.#lastAppliedToolSignature = signature;
|
|
3577
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
3495
3578
|
}
|
|
3496
3579
|
}
|
|
3497
3580
|
if (options?.persistMCPSelection !== false) {
|
|
@@ -3574,6 +3657,7 @@ export class AgentSession {
|
|
|
3574
3657
|
const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
|
3575
3658
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
3576
3659
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
3660
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
3577
3661
|
// Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
|
|
3578
3662
|
// the same tool set does not re-rebuild on top of the explicit refresh we
|
|
3579
3663
|
// just performed (and conversely, a different set forces a fresh rebuild).
|
|
@@ -3633,7 +3717,7 @@ export class AgentSession {
|
|
|
3633
3717
|
* closure-captured ones cannot change at runtime regardless of skip behavior.
|
|
3634
3718
|
* For everything else, callers must explicitly call `refreshBaseSystemPrompt()`
|
|
3635
3719
|
* after side-effecting changes; see e.g. the memory hooks and
|
|
3636
|
-
* `#
|
|
3720
|
+
* `#syncAfterModelChange`.
|
|
3637
3721
|
*
|
|
3638
3722
|
* The current calendar date IS covered (appended as a segment) because
|
|
3639
3723
|
* `buildSystemPrompt` injects it into the prompt body (`Today is '{{date}}'`).
|
|
@@ -5225,7 +5309,7 @@ export class AgentSession {
|
|
|
5225
5309
|
// Re-apply thinking for the newly selected model. Prefer the model's
|
|
5226
5310
|
// configured defaultLevel; otherwise preserve the current level (or auto).
|
|
5227
5311
|
this.#reapplyThinkingLevel(model.thinking?.defaultLevel);
|
|
5228
|
-
await this.#
|
|
5312
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5229
5313
|
}
|
|
5230
5314
|
|
|
5231
5315
|
/**
|
|
@@ -5259,7 +5343,7 @@ export class AgentSession {
|
|
|
5259
5343
|
} else {
|
|
5260
5344
|
this.#reapplyThinkingLevel(model.thinking?.defaultLevel);
|
|
5261
5345
|
}
|
|
5262
|
-
await this.#
|
|
5346
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5263
5347
|
}
|
|
5264
5348
|
|
|
5265
5349
|
/**
|
|
@@ -5403,7 +5487,7 @@ export class AgentSession {
|
|
|
5403
5487
|
|
|
5404
5488
|
// Apply the scoped model's configured thinking level, preserving auto.
|
|
5405
5489
|
this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : next.thinkingLevel);
|
|
5406
|
-
await this.#
|
|
5490
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5407
5491
|
|
|
5408
5492
|
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
|
5409
5493
|
}
|
|
@@ -5432,7 +5516,7 @@ export class AgentSession {
|
|
|
5432
5516
|
this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
|
5433
5517
|
// Re-apply the current thinking level (or auto) for the newly selected model
|
|
5434
5518
|
this.#reapplyThinkingLevel();
|
|
5435
|
-
await this.#
|
|
5519
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5436
5520
|
|
|
5437
5521
|
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
|
|
5438
5522
|
}
|
|
@@ -12,12 +12,16 @@ export type {
|
|
|
12
12
|
AuthStorageOptions,
|
|
13
13
|
OAuthCredential,
|
|
14
14
|
SerializedAuthStorage,
|
|
15
|
+
SnapshotResponse,
|
|
15
16
|
StoredAuthCredential,
|
|
16
17
|
} from "@oh-my-pi/pi-ai";
|
|
17
18
|
export {
|
|
18
19
|
AuthBrokerClient,
|
|
19
20
|
AuthStorage,
|
|
21
|
+
DEFAULT_SNAPSHOT_CACHE_TTL_MS,
|
|
20
22
|
REMOTE_REFRESH_SENTINEL,
|
|
21
23
|
RemoteAuthCredentialStore,
|
|
24
|
+
readAuthBrokerSnapshotCache,
|
|
22
25
|
SqliteAuthCredentialStore,
|
|
26
|
+
writeAuthBrokerSnapshotCache,
|
|
23
27
|
} from "@oh-my-pi/pi-ai";
|