Files
hapi/cli/src/agent/sessionConfigRpc.ts
T
SSU-WEI HUANGandGitHub b9eed7c071 feat: add Grok Build support (#1030)
* test: define Grok Build integration behavior

* feat: add Grok Build agent integration

* test: cover Grok permissions and resume paths

* docs: add Grok Build setup guide

* fix: scope Grok ACP discovery to session cwd

* fix: align Grok permission UI semantics

* docs: clarify Grok runner setup

* test: require Grok create model and effort options

* feat: add Grok create model and effort pickers

* test: define Grok runtime parity behavior

* feat: add Grok runtime ACP controls and discovery

* fix: tighten Grok runtime controls

* fix: suppress nonfatal Grok title quota errors

* feat: support Grok Auto permission mode

* feat: forward ACP native session titles for Grok

* fix: guard Grok Windows shell arguments
2026-07-13 08:41:30 +08:00

123 lines
4.6 KiB
TypeScript

import { isPermissionModeAllowedForFlavor, type AgentFlavor } from '@hapi/protocol'
import { RPC_METHODS } from '@hapi/protocol/rpcMethods'
import { PermissionModeSchema } from '@hapi/protocol/schemas'
import type { PermissionMode } from '@hapi/protocol/types'
import type { RpcHandlerManager } from '@/api/rpc/RpcHandlerManager'
type SessionConfigState<TPermissionMode extends PermissionMode = PermissionMode> = {
permissionMode?: TPermissionMode
model?: string | null
modelReasoningEffort?: string | null
effort?: string | null
}
type RegisterSessionConfigRpcOptions<TPermissionMode extends PermissionMode = PermissionMode> = {
rpcHandlerManager: RpcHandlerManager
flavor: AgentFlavor
modelMode?: 'nullable' | 'ignore' | 'reject'
modelReasoningEffortMode?: 'nullable' | 'ignore' | 'reject'
effortMode?: 'nullable' | 'ignore' | 'reject'
appliedFallback?: () => Record<string, unknown>
onApply: (config: SessionConfigState<TPermissionMode>) => void
onAfterApply?: () => void
}
export function resolveSessionConfigPermissionMode<TPermissionMode extends PermissionMode>(
value: unknown,
flavor: AgentFlavor
): TPermissionMode {
const parsed = PermissionModeSchema.safeParse(value)
if (!parsed.success || !isPermissionModeAllowedForFlavor(parsed.data, flavor)) {
throw new Error('Invalid permission mode')
}
return parsed.data as TPermissionMode
}
/** Extract `modelId` from either a plain string or a `{ provider, modelId }`
* object (the form Pi sessions receive from the hub). Other agents only pass
* plain strings; the object branch is here for schema consistency so this
* function doesn't throw if the hub later sends the union form to any agent. */
export function resolveNullableSessionModel(value: unknown): string | null {
if (value === null) {
return null
}
// Pi sessions receive model as { provider, modelId }; extract modelId
if (typeof value === 'object' && value !== null) {
const modelObj = value as { modelId?: unknown }
if (typeof modelObj.modelId === 'string' && modelObj.modelId.trim().length > 0) {
return modelObj.modelId.trim()
}
throw new Error('Invalid model')
}
if (typeof value !== 'string' || value.trim().length === 0) {
throw new Error('Invalid model')
}
return value.trim()
}
export function registerSessionConfigRpc<TPermissionMode extends PermissionMode>({
rpcHandlerManager,
flavor,
modelMode = 'reject',
modelReasoningEffortMode = 'reject',
effortMode = 'reject',
appliedFallback,
onApply,
onAfterApply
}: RegisterSessionConfigRpcOptions<TPermissionMode>): void {
rpcHandlerManager.registerHandler(RPC_METHODS.SetSessionConfig, async (payload: unknown) => {
if (!payload || typeof payload !== 'object') {
throw new Error('Invalid session config payload')
}
const config = payload as { permissionMode?: unknown; model?: unknown; modelReasoningEffort?: unknown; effort?: unknown }
const applied: Record<string, unknown> = {}
const next: SessionConfigState<TPermissionMode> = {}
if (config.permissionMode !== undefined) {
next.permissionMode = resolveSessionConfigPermissionMode<TPermissionMode>(config.permissionMode, flavor)
applied.permissionMode = next.permissionMode
}
if (config.model !== undefined) {
if (modelMode === 'reject') {
throw new Error('Invalid model')
}
if (modelMode === 'nullable') {
next.model = resolveNullableSessionModel(config.model)
applied.model = next.model
}
}
if (config.modelReasoningEffort !== undefined) {
if (modelReasoningEffortMode === 'reject') {
throw new Error('Invalid model reasoning effort')
}
if (modelReasoningEffortMode === 'nullable') {
next.modelReasoningEffort = resolveNullableSessionModel(config.modelReasoningEffort)
applied.modelReasoningEffort = next.modelReasoningEffort
}
}
if (config.effort !== undefined) {
if (effortMode === 'reject') {
throw new Error('Invalid effort')
}
if (effortMode === 'nullable') {
next.effort = resolveNullableSessionModel(config.effort)
applied.effort = next.effort
}
}
onApply(next)
onAfterApply?.()
return {
applied: Object.keys(applied).length > 0
? applied
: (appliedFallback?.() ?? applied)
}
})
}