- resolve the agent dir through the host getAgentDir() (registered by the composition root) instead of sniffing execPath/argv - synthesize agent_settled from the first terminal agent_end on omp - read the live model through ctx.models.current() and prefer it over the stored model on omp - adapt omp Settings (reloadFromDisk, global enabledModels) at the SDK boundary and drop scoped-model persistence on omp
This commit is contained in:
1 parent
52c9fbeaf3
commit
2aefc7ef5a
12 files changed
+232
-55
No files matched your search
@@ -639,6 +639,7 @@ export function registerTelegramLifecycleRuntimeHooks({
|
||||
if (!isSessionContextActive(ctx)) return;
|
||||
activityRuntime.onAgentSettled();
|
||||
},
|
||||
settleOnTerminalAgentEnd: Pi.isOmpHost(pi),
|
||||
onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
|
||||
isConfigured: configStore.hasBotToken,
|
||||
isProactivePushEnabled,
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
ToolExecutionStartEvent,
|
||||
ToolExecutionUpdateEvent,
|
||||
} from "./pi.ts";
|
||||
import { isTerminalAgentEnd } from "./pi.ts";
|
||||
|
||||
let resetTransportReplyDedupFn: (() => void) | undefined;
|
||||
|
||||
@@ -110,6 +111,12 @@ export interface TelegramLifecycleRegistrationDeps {
|
||||
event: AgentSettledEvent,
|
||||
ctx: ExtensionContext,
|
||||
) => Promise<void> | void;
|
||||
/**
|
||||
* Hosts that never emit `agent_settled` (omp) get it synthesized from the
|
||||
* first terminal `agent_end`. Leave unset on hosts that emit it natively so
|
||||
* the settle point is not announced early.
|
||||
*/
|
||||
settleOnTerminalAgentEnd?: boolean;
|
||||
}
|
||||
|
||||
export interface TelegramSessionLifecycleHooks {
|
||||
@@ -562,6 +569,9 @@ export function registerTelegramLifecycleHooks(
|
||||
pi.on("agent_end", async (event, ctx) => {
|
||||
if (!isActive(ctx)) return;
|
||||
await deps.onAgentEnd(event, ctx);
|
||||
if (deps.settleOnTerminalAgentEnd && isTerminalAgentEnd(event)) {
|
||||
await deps.onAgentSettled?.({ type: "agent_settled" }, ctx);
|
||||
}
|
||||
});
|
||||
pi.on("agent_settled", async (event, ctx) => {
|
||||
if (!isActive(ctx)) return;
|
||||
|
||||
+16
-2
@@ -55,15 +55,25 @@ export type CurrentModelRuntime<
|
||||
> = CurrentModelStore<TContext, TModel> &
|
||||
CurrentModelUpdateRuntime<TContext, TModel>;
|
||||
|
||||
/**
|
||||
* `preferContextModel` makes the host's live context model win over the stored
|
||||
* one. Pi pushes model changes through `model_select`, so its stored value is
|
||||
* current; omp has no such event, so its stored value would go stale after a
|
||||
* local `/model` switch.
|
||||
*/
|
||||
export function createCurrentModelStore<
|
||||
TContext,
|
||||
TModel extends MenuModel = MenuModel,
|
||||
>(
|
||||
getContextModel: (ctx: TContext) => TModel | undefined,
|
||||
preferContextModel = false,
|
||||
): CurrentModelStore<TContext, TModel> {
|
||||
let currentModel: TModel | undefined;
|
||||
return {
|
||||
get: (ctx) => currentModel ?? getContextModel(ctx),
|
||||
get: (ctx) =>
|
||||
preferContextModel
|
||||
? (getContextModel(ctx) ?? currentModel)
|
||||
: (currentModel ?? getContextModel(ctx)),
|
||||
getStored: () => currentModel,
|
||||
set: (model) => {
|
||||
currentModel = model;
|
||||
@@ -96,8 +106,12 @@ export function createCurrentModelRuntime<
|
||||
>(deps: {
|
||||
getContextModel: (ctx: TContext) => TModel | undefined;
|
||||
updateStatus: (ctx: TContext) => void;
|
||||
preferContextModel?: boolean;
|
||||
}): CurrentModelRuntime<TContext, TModel> {
|
||||
const store = createCurrentModelStore(deps.getContextModel);
|
||||
const store = createCurrentModelStore(
|
||||
deps.getContextModel,
|
||||
deps.preferContextModel,
|
||||
);
|
||||
return {
|
||||
...store,
|
||||
...createCurrentModelUpdateRuntime({
|
||||
|
||||
+25
-17
@@ -1,19 +1,32 @@
|
||||
/**
|
||||
* Telegram bridge path resolution for Pi-compatible runtimes
|
||||
* Zones: telemetry paths, filesystem, runtime identity
|
||||
* Owns agent-dir detection and extension-local path derivation
|
||||
* Owns agent-dir resolution and extension-local path derivation
|
||||
*
|
||||
* This domain is pure/path-only: it resolves directories and file paths
|
||||
* from environment and runtime identity. It does not read config, manage
|
||||
* state, or import broader Telegram domains.
|
||||
* This domain is pure/path-only: it resolves directories and file paths from
|
||||
* the environment and the host runtime's own agent directory. It does not read
|
||||
* config, manage state, or import broader Telegram domains or the Pi SDK.
|
||||
*/
|
||||
import { homedir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
export interface TelegramAgentDirResolutionInput {
|
||||
env?: Partial<Pick<NodeJS.ProcessEnv, "PI_CODING_AGENT_DIR">>;
|
||||
execPath?: string;
|
||||
argv?: readonly string[];
|
||||
}
|
||||
|
||||
let hostAgentDirResolver: (() => string) | undefined;
|
||||
|
||||
/**
|
||||
* Register the host runtime's own agent-directory lookup (the Pi SDK
|
||||
* `getAgentDir()`). It is registered by the composition root instead of
|
||||
* imported here: importing the Pi SDK from this leaf made every domain that
|
||||
* touches a path, and every child process the test suite spawns, pay about a
|
||||
* second of SDK startup.
|
||||
*/
|
||||
export function setHostAgentDirResolver(
|
||||
resolver: (() => string) | undefined,
|
||||
): void {
|
||||
hostAgentDirResolver = resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -21,23 +34,18 @@ export interface TelegramAgentDirResolutionInput {
|
||||
*
|
||||
* Precedence:
|
||||
* 1. `PI_CODING_AGENT_DIR` env variable, when explicitly set.
|
||||
* 2. Detect Pi-compatible runtime identity from the executable or argv[1]
|
||||
* (e.g. OMP vs standard Pi agent).
|
||||
* 3. Fallback: `~/.pi/agent`.
|
||||
* 2. The registered host agent directory. Pi returns `~/.pi/agent`; omp's Pi
|
||||
* shim returns its profile-aware directory (default `~/.omp/agent`). The
|
||||
* executable and argv are not inspected: omp installed through Bun runs as
|
||||
* `bun .../dist/cli.js`, which neither contains nor starts with `omp`.
|
||||
* 3. Fallback when no host is registered: `~/.pi/agent`.
|
||||
*/
|
||||
export function resolveAgentDir(
|
||||
input: TelegramAgentDirResolutionInput = {},
|
||||
): string {
|
||||
const env = input.env ?? process.env;
|
||||
if (env.PI_CODING_AGENT_DIR) return resolve(env.PI_CODING_AGENT_DIR);
|
||||
const execPath = input.execPath ?? process.execPath;
|
||||
const argv = input.argv ?? process.argv;
|
||||
const execBasename = execPath.toLowerCase().split(/[\\/]/u).pop() ?? "";
|
||||
const argv1Last = (argv[1] ?? "").toLowerCase().split(/[\\/]/u).pop() ?? "";
|
||||
if (execBasename.startsWith("omp") || argv1Last.startsWith("omp")) {
|
||||
return join(homedir(), ".omp", "agent");
|
||||
}
|
||||
return join(homedir(), ".pi", "agent");
|
||||
return hostAgentDirResolver?.() ?? join(homedir(), ".pi", "agent");
|
||||
}
|
||||
|
||||
/** Telegram bridge configuration file (<agentDir>/telegram.json). */
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type SessionStartEvent,
|
||||
type SlashCommandInfo,
|
||||
SettingsManager,
|
||||
getAgentDir,
|
||||
} from "@earendil-works/pi-coding-agent";
|
||||
|
||||
export type {
|
||||
@@ -39,6 +40,13 @@ export type {
|
||||
SlashCommandInfo,
|
||||
};
|
||||
|
||||
/**
|
||||
* Agent data directory owned by the host runtime. Pi returns its own root and
|
||||
* omp's legacy Pi shim forwards omp's profile-aware directory, so no argv or
|
||||
* executable sniffing is needed.
|
||||
*/
|
||||
export { getAgentDir as getHostAgentDir };
|
||||
|
||||
export interface ToolExecutionStartEvent {
|
||||
type: "tool_execution_start";
|
||||
toolCallId: string;
|
||||
@@ -69,6 +77,26 @@ export interface PiSettingsManager {
|
||||
setEnabledModels: (patterns: string[] | undefined) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* omp's `Settings` is what its legacy shim returns from
|
||||
* `SettingsManager.create()`. It has no `reload()` or `get/setEnabledModels()`:
|
||||
* it reloads through `reloadFromDisk()` and keeps `enabledModels` as a
|
||||
* path-scoped typed setting.
|
||||
*/
|
||||
interface OmpSettingsSurface {
|
||||
reloadFromDisk: () => Promise<void>;
|
||||
flush: () => Promise<void>;
|
||||
getGlobalSettings: () => { enabledModels?: unknown };
|
||||
}
|
||||
|
||||
function isOmpSettings(value: object): value is OmpSettingsSurface {
|
||||
return (
|
||||
"reloadFromDisk" in value &&
|
||||
"flush" in value &&
|
||||
"getGlobalSettings" in value
|
||||
);
|
||||
}
|
||||
|
||||
export type PiSlashCommandInfo = SlashCommandInfo;
|
||||
export type PiRunMode = "tui" | "rpc" | "json" | "print";
|
||||
|
||||
@@ -150,7 +178,22 @@ export function createExtensionApiRuntimePorts(
|
||||
}
|
||||
|
||||
export function createSettingsManager(cwd: string): PiSettingsManager {
|
||||
return SettingsManager.create(cwd);
|
||||
const manager: PiSettingsManager = SettingsManager.create(cwd);
|
||||
if (!isOmpSettings(manager)) return manager;
|
||||
return {
|
||||
reload: () => manager.reloadFromDisk(),
|
||||
flush: () => manager.flush(),
|
||||
getEnabledModels: () => {
|
||||
const enabled = manager.getGlobalSettings().enabledModels;
|
||||
return Array.isArray(enabled) &&
|
||||
enabled.every((pattern) => typeof pattern === "string")
|
||||
? enabled
|
||||
: undefined;
|
||||
},
|
||||
setEnabledModels: () => {
|
||||
throw new Error("Scoped model persistence is not supported on omp.");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createScopedModelPatternPersister(deps: {
|
||||
@@ -167,16 +210,56 @@ export function createScopedModelPatternPersister(deps: {
|
||||
};
|
||||
}
|
||||
|
||||
interface OmpModelQuery {
|
||||
current: () => ExtensionContext["model"];
|
||||
}
|
||||
|
||||
function hasOmpModelQuery(
|
||||
ctx: ExtensionContext,
|
||||
): ctx is ExtensionContext & { models: OmpModelQuery } {
|
||||
return (
|
||||
"models" in ctx &&
|
||||
typeof ctx.models === "object" &&
|
||||
ctx.models !== null &&
|
||||
"current" in ctx.models &&
|
||||
typeof ctx.models.current === "function"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* omp builds command contexts by spreading a base context, which evaluates the
|
||||
* `model` getter once: a context stored from `/telegram-connect` would report
|
||||
* the model of that moment forever. omp's `ctx.models.current()` closes over
|
||||
* the live session model, so prefer it when present.
|
||||
*/
|
||||
export function getExtensionContextModel(
|
||||
ctx: ExtensionContext,
|
||||
): ExtensionContext["model"] {
|
||||
return ctx.model;
|
||||
return hasOmpModelQuery(ctx) ? ctx.models.current() : ctx.model;
|
||||
}
|
||||
|
||||
export function getExtensionContextCwd(ctx: ExtensionContext): string {
|
||||
return ctx.cwd;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the extension API belongs to omp. omp's `ExtensionAPI` carries
|
||||
* `typebox` and `pi` module facades that upstream Pi's API does not have.
|
||||
*/
|
||||
export function isOmpHost(api: ExtensionAPI): boolean {
|
||||
return "typebox" in api && "pi" in api;
|
||||
}
|
||||
|
||||
/**
|
||||
* omp marks an `agent_end` with `willContinue: true` whenever it already
|
||||
* scheduled a retry, compaction continuation, todo/plan reminder, or async
|
||||
* wake. Any other `agent_end` is terminal, which is what Pi reports through
|
||||
* `agent_settled`.
|
||||
*/
|
||||
export function isTerminalAgentEnd(event: AgentEndEvent): boolean {
|
||||
return !("willContinue" in event && event.willContinue === true);
|
||||
}
|
||||
|
||||
export function isExtensionContextIdle(ctx: ExtensionContext): boolean {
|
||||
return ctx.isIdle();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user