Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/permission-mode-reminder-env.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/configuration/env-vars.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading