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 2a5817e99..1d28e3897 100644 --- a/docs/configuration/env-vars.md +++ b/docs/configuration/env-vars.md @@ -141,6 +141,7 @@ Switches that control the behavior of subsystems such as telemetry, background t | `PYTHINKER_CODE_BUILTIN_PRODUCT_SKILLS` | Whether the built-in skills documenting Pythinker Code itself are offered to the model; takes higher priority than `builtin_product_skills` in `config.toml` (default enabled) | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | | `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_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_CODE_SEARCH_WORKER` | Run the global search index in a dedicated worker thread; takes higher priority than `[database] search` in `config.toml` (default `true`) | Truthy: `1`/`true`/`yes`/`on`; falsy: `0`/`false`/`no`/`off` | 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 3b295755b..2f2b338b8 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,