File size: 2,937 Bytes
f0634fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
 * ACP session-mode taxonomy.
 *
 * The 4 modes (`default`, `plan`, `auto`, `yolo`) are the locked decision.
 * Every `session/new` and `session/load` response advertises {@link ACP_MODES}
 * as the mode picker plus {@link DEFAULT_MODE_ID} as `currentModeId`, so ACP
 * clients render the dropdown from a single canonical source.
 *
 * `session/set_mode` and the `mode` arm of `session/set_config_option` consume
 * the same source of truth: {@link isAcpModeId} narrows the wire string, and
 * {@link acpModeToToggles} resolves the two underlying engine toggles (plan
 * mode + permission mode) each ACP mode maps to.
 */

import type { SessionMode } from '@agentclientprotocol/sdk';
import type { PermissionMode } from '@moonshot-ai/agent-core-v2';

/**
 * Canonical 4-mode taxonomy. Order matters: the array is rendered as-is by the
 * client, so `default` must appear first and `yolo` last.
 */
export const ACP_MODES = [
  {
    id: 'default',
    name: 'Default',
    description: 'Manual approvals; tools execute normally.',
  },
  {
    id: 'plan',
    name: 'Plan',
    description: 'Read-only planning; no tool execution.',
  },
  {
    id: 'auto',
    name: 'Auto',
    description: 'Auto-approve safe operations.',
  },
  {
    id: 'yolo',
    name: 'YOLO',
    description: 'Auto-approve everything.',
  },
] as const satisfies readonly SessionMode[];

/** Initial `currentModeId` for every freshly created ACP session. */
export const DEFAULT_MODE_ID = 'default' as const;

/** The four wire-level mode ids understood by this host. */
export type AcpModeId = 'default' | 'plan' | 'auto' | 'yolo';

/** Narrow an unknown wire string to {@link AcpModeId}. */
export function isAcpModeId(value: unknown): value is AcpModeId {
  return value === 'default' || value === 'plan' || value === 'auto' || value === 'yolo';
}

/**
 * The two underlying engine toggles each ACP mode maps to. `plan` drives
 * `IAgentPlanService` (enter/exit plan mode) and `permission` drives
 * `IAgentPermissionModeService.setMode`.
 */
export interface AcpModeToggles {
  readonly plan: boolean;
  readonly permission: PermissionMode;
}

/**
 * Resolve an {@link AcpModeId} to its underlying engine toggles. The `switch`
 * deliberately enumerates every arm of {@link AcpModeId} so the compiler
 * enforces exhaustiveness — adding a 5th mode without extending this table is
 * a typecheck error (the `never` fallthrough), not a silent runtime no-op.
 */
export function acpModeToToggles(id: AcpModeId): AcpModeToggles {
  switch (id) {
    case 'default':
      return { plan: false, permission: 'manual' };
    case 'plan':
      return { plan: true, permission: 'manual' };
    case 'auto':
      return { plan: false, permission: 'auto' };
    case 'yolo':
      return { plan: false, permission: 'yolo' };
    default: {
      const _exhaustive: never = id;
      throw new Error(`Unhandled AcpModeId: ${String(_exhaustive)}`);
    }
  }
}