From c19e89751166234f74a17198102a17d581fafbaa Mon Sep 17 00:00:00 2001 From: elkaix Date: Sun, 13 Sep 2026 21:29:38 -0400 Subject: [PATCH] feat(agent-core-v2): add the permission mode reminder env switch Set PYTHINKER_CODE_PERMISSION_MODE_REMINDER to a false value to stop injecting the auto permission-mode reminder into the model context. --- .changeset/permission-mode-reminder-env.md | 5 ++ docs/configuration/env-vars.md | 1 + .../permissionMode/permissionModeService.ts | 18 +++-- .../permissionMode/permissionMode.test.ts | 80 ++++++++++++++++++- .../agentLifecycle/agentLifecycle.test.ts | 1 + 5 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 .changeset/permission-mode-reminder-env.md diff --git a/.changeset/permission-mode-reminder-env.md b/.changeset/permission-mode-reminder-env.md new file mode 100644 index 000000000..a96e96e7c --- /dev/null +++ b/.changeset/permission-mode-reminder-env.md @@ -0,0 +1,5 @@ +--- +"@pymodel/pythinker-code": minor +--- + +Add the `PYTHINKER_CODE_PERMISSION_MODE_REMINDER` environment variable: set it to a false value or an empty value to stop injecting the auto permission-mode reminders into the model context. diff --git a/docs/configuration/env-vars.md b/docs/configuration/env-vars.md index b010de95b..a0203f5be 100644 --- a/docs/configuration/env-vars.md +++ b/docs/configuration/env-vars.md @@ -142,6 +142,7 @@ Switches that control the behavior of subsystems such as telemetry, background t | `PYTHINKER_CODE_TUI_FULL_SCREEN` | Control the fullscreen TUI with a fixed prompt dock, scrollable transcript, mouse text selection, clickable links, transcript search, and a clickable jump-to-bottom control. Fullscreen is enabled by default | `0` restores the legacy inline UI; unset or any other value keeps fullscreen enabled | | `PYTHINKER_CODE_EXPERIMENTAL_SECONDARY_MODEL` | Control the [subagent model pool](./config-files.md#subagent-model-pool) in every launch mode, including the interactive TUI. It is enabled by default; set this variable to a false value to disable it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | | `PYTHINKER_CODE_DANGEROUS_COMMAND_GUARD` | Override [`[permission].dangerous_command_guard`](./config-files.md#permission). The guard asks before dangerous or unanalyzable `Bash` commands in interactive modes and blocks them in Auto mode | `true` or `false`; default `true` | +| `PYTHINKER_CODE_PERMISSION_MODE_REMINDER` | Stop injecting the automatic permission-mode reminder (the context note that explains Auto mode) into the model context; set to a false value or an empty value to disable | Truthy keeps the reminder enabled; falsy: `0`/`false`/`no`/`off` disables it; an empty value disables it | | `PYTHINKER_CODE_EXPERIMENTAL_SUBAGENT_FORK` | Enable the experimental `fork` parameter on the `Agent` and `AgentDynamicWorkflow` tools, letting the model start a subagent with a snapshot of the calling agent's conversation history instead of an empty context; the master `PYTHINKER_CODE_EXPERIMENTAL_FLAG=1` also enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | | `PYTHINKER_CODE_EXPERIMENTAL_TOWER` | Enable the experimental [`/tower`](../reference/slash-commands.md#modes--run-control) command for workspace-wide subagent coordination; the master `PYTHINKER_CODE_EXPERIMENTAL_FLAG=1` also enables it | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | | `PYTHINKER_MCP_STARTUP_TIMEOUT_MS` | Global default connection timeout (ms) for all MCP servers; takes higher priority than `[mcp] startup_timeout_ms` in `config.toml`, but a per-server `startupTimeoutMs` in `mcp.json` still wins (default `30000`) | Integer from `1` to `2147483647`; invalid values are ignored | diff --git a/packages/agent-core-v2/src/agent/permissionMode/permissionModeService.ts b/packages/agent-core-v2/src/agent/permissionMode/permissionModeService.ts index 0f0f7a7d3..b07b30838 100644 --- a/packages/agent-core-v2/src/agent/permissionMode/permissionModeService.ts +++ b/packages/agent-core-v2/src/agent/permissionMode/permissionModeService.ts @@ -1,5 +1,7 @@ import type { PermissionMode } from '#/agent/permissionPolicy/types'; import { Service } from '#/_base/di/service'; +import { parseBooleanEnv } from '#/_base/utils/env'; +import { IBootstrapService } from '#/app/bootstrap/bootstrap'; import { LifecycleScope } from '#/app/scopes'; import { ScopeActivation, registerScopedService } from '#/_base/di/scope'; import { Emitter, type Event } from '#/_base/event'; @@ -20,6 +22,8 @@ import { PermissionSetMode, } from './permissionModeOps'; +export const PERMISSION_MODE_REMINDER_ENV = 'PYTHINKER_CODE_PERMISSION_MODE_REMINDER'; + export class AgentPermissionModeService extends Service implements IAgentPermissionModeService { declare readonly _serviceBrand: undefined; @@ -32,15 +36,19 @@ export class AgentPermissionModeService extends Service implements IAgentPermiss @IAgentLifecycleService private readonly agentLifecycle: IAgentLifecycleService, @ITelemetryService private readonly telemetry: ITelemetryService, @IAgentStateService private readonly agentState: IAgentStateService, + @IBootstrapService bootstrap: IBootstrapService, ) { super(); this.agentState.contributeState(permissionModeKey); this.agentState.contributeState(permissionModeConfiguredKey); - this._register( - activateReminderWhenReady(this.agentLifecycle, this.scopeContext, (reminder) => - new PermissionModeInjection(this, reminder, this.agentState), - ), - ); + const reminderEnv = bootstrap.getEnv(PERMISSION_MODE_REMINDER_ENV); + if (reminderEnv?.trim() !== '' && parseBooleanEnv(reminderEnv) !== false) { + this._register( + activateReminderWhenReady(this.agentLifecycle, this.scopeContext, (reminder) => + new PermissionModeInjection(this, reminder, this.agentState), + ), + ); + } } get mode(): PermissionMode { diff --git a/packages/agent-core-v2/test/agent/permissionMode/permissionMode.test.ts b/packages/agent-core-v2/test/agent/permissionMode/permissionMode.test.ts index 5aafbe5a4..8ea612a84 100644 --- a/packages/agent-core-v2/test/agent/permissionMode/permissionMode.test.ts +++ b/packages/agent-core-v2/test/agent/permissionMode/permissionMode.test.ts @@ -7,9 +7,14 @@ import type { ReminderRuntime } from '#/features/reminder/reminderAgentRuntime'; import type { ContextInjectionProvider } from '#/features/reminder/types'; import { IAgentLifecycleService } from '#/session/agentLifecycle/agentLifecycle'; import { lifecycleWithReminder } from '../../features/reminder/stubs'; +import { IBootstrapService } from '#/app/bootstrap/bootstrap'; +import { stubBootstrap } from '../../app/bootstrap/stubs'; import { IAgentPermissionModeService } from '#/agent/permissionMode/permissionMode'; import { PermissionModeInjection } from '#/agent/permissionMode/injection/permissionModeInjection'; -import { AgentPermissionModeService } from '#/agent/permissionMode/permissionModeService'; +import { + AgentPermissionModeService, + PERMISSION_MODE_REMINDER_ENV, +} from '#/agent/permissionMode/permissionModeService'; import { permissionModeKey } from '#/agent/permissionMode/permissionModeOps'; import type { PermissionMode } from '#/agent/permissionPolicy/types'; import { IAgentStateService } from '#/agent/state/agentState'; @@ -57,15 +62,18 @@ let log: IAppendLogStore; let dispatcher: IEventDispatcher; let svc: IAgentPermissionModeService; let reminderLive = false; +let bootstrapEnv: NodeJS.ProcessEnv; beforeEach(() => { registeredInjection = undefined; reminderLive = false; + bootstrapEnv = {}; disposables = new DisposableStore(); ix = disposables.add(new TestInstantiationService()); ix.stub(IFileSystemStorageService, new InMemoryStorageService()); ix.set(IAppendLogStore, new SyncDescriptor(AppendLogStore)); ix.stub(IAgentLifecycleService, lifecycleWithReminder(injectorStub)); + ix.stub(IBootstrapService, stubBootstrap('/tmp/pythinker-home', bootstrapEnv)); ix.set(IAgentStateService, new AgentStateService()); ix.set(IAgentPermissionModeService, new SyncDescriptor(AgentPermissionModeService)); log = ix.get(IAppendLogStore); @@ -243,4 +251,74 @@ describe('AgentPermissionModeService (wire-backed)', () => { expect(written[0]).toMatchObject({ type: 'metadata' }); expect(written.slice(1)).toEqual([{ type: 'permission.set_mode', mode: 'auto' }]); }); + it('skips the auto-mode reminder injection when the reminder env is disabled', () => { + registeredInjection = undefined; + const ix2 = disposables.add(new TestInstantiationService()); + ix2.stub(IFileSystemStorageService, new InMemoryStorageService()); + ix2.set(IAppendLogStore, new SyncDescriptor(AppendLogStore)); + ix2.stub(IAgentLifecycleService, lifecycleWithReminder(injectorStub)); + ix2.stub( + IBootstrapService, + stubBootstrap('/tmp/pythinker-home', { [PERMISSION_MODE_REMINDER_ENV]: '0' }), + ); + ix2.set(IAgentStateService, new AgentStateService()); + ix2.set(IAgentPermissionModeService, new SyncDescriptor(AgentPermissionModeService)); + registerTestAgentWire(ix2, testWireScope(SCOPE, 'permission-mode-no-reminder'), { + log: ix2.get(IAppendLogStore), + }); + registerTestEventDispatcher(ix2); + + const svc2 = ix2.get(IAgentPermissionModeService); + + expect(registeredInjection).toBeUndefined(); + svc2.setMode('auto'); + expect(svc2.mode).toBe('auto'); + expect(registeredInjection).toBeUndefined(); + }); + it('skips the auto-mode reminder injection when the reminder env is set to an empty value', () => { + registeredInjection = undefined; + const ix2 = disposables.add(new TestInstantiationService()); + ix2.stub(IFileSystemStorageService, new InMemoryStorageService()); + ix2.set(IAppendLogStore, new SyncDescriptor(AppendLogStore)); + ix2.stub(IAgentLifecycleService, lifecycleWithReminder(injectorStub)); + ix2.stub( + IBootstrapService, + stubBootstrap('/tmp/pythinker-home', { [PERMISSION_MODE_REMINDER_ENV]: '' }), + ); + ix2.set(IAgentStateService, new AgentStateService()); + ix2.set(IAgentPermissionModeService, new SyncDescriptor(AgentPermissionModeService)); + registerTestAgentWire(ix2, testWireScope(SCOPE, 'permission-mode-empty-reminder'), { + log: ix2.get(IAppendLogStore), + }); + registerTestEventDispatcher(ix2); + + const svc2 = ix2.get(IAgentPermissionModeService); + + expect(registeredInjection).toBeUndefined(); + svc2.setMode('auto'); + expect(svc2.mode).toBe('auto'); + expect(registeredInjection).toBeUndefined(); + }); + + it('keeps the auto-mode reminder injection when the env override enables it explicitly', () => { + registeredInjection = undefined; + const ix2 = disposables.add(new TestInstantiationService()); + ix2.stub(IFileSystemStorageService, new InMemoryStorageService()); + ix2.set(IAppendLogStore, new SyncDescriptor(AppendLogStore)); + ix2.stub(IAgentLifecycleService, lifecycleWithReminder(injectorStub)); + ix2.stub( + IBootstrapService, + stubBootstrap('/tmp/pythinker-home', { [PERMISSION_MODE_REMINDER_ENV]: '1' }), + ); + ix2.set(IAgentStateService, new AgentStateService()); + ix2.set(IAgentPermissionModeService, new SyncDescriptor(AgentPermissionModeService)); + registerTestAgentWire(ix2, testWireScope(SCOPE, 'permission-mode-reminder-on'), { + log: ix2.get(IAppendLogStore), + }); + registerTestEventDispatcher(ix2); + + ix2.get(IAgentPermissionModeService); + + expect((registeredInjection as { readonly name: string } | undefined)?.name).toBe('permission_mode'); + }); }); diff --git a/packages/agent-core-v2/test/session/agentLifecycle/agentLifecycle.test.ts b/packages/agent-core-v2/test/session/agentLifecycle/agentLifecycle.test.ts index 965cd3e92..9888fcf6b 100644 --- a/packages/agent-core-v2/test/session/agentLifecycle/agentLifecycle.test.ts +++ b/packages/agent-core-v2/test/session/agentLifecycle/agentLifecycle.test.ts @@ -265,6 +265,7 @@ describe('AgentLifecycleService', () => { _serviceBrand: undefined, homeDir: '/tmp/pythinker-agentLifecycle-home', cwd: '/tmp/pythinker-agentLifecycle-home', + getEnv: () => undefined, } as unknown as IBootstrapService); ix.stub(ISessionWorkspaceContext, { _serviceBrand: undefined,