mirror of
https://github.com/openai/codex.git
synced 2026-10-25 18:28:38 +00:00
Add file-backed personality support and listing
This commit is contained in:
Generated
+1
@@ -1898,6 +1898,7 @@ dependencies = [
|
||||
"seccompiler",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"serial_test",
|
||||
"sha1",
|
||||
"shlex",
|
||||
|
||||
@@ -1304,13 +1304,19 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
"friendly",
|
||||
"pragmatic"
|
||||
],
|
||||
"type": "string"
|
||||
"PersonalitiesListParams": {
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"PluginInstallParams": {
|
||||
"properties": {
|
||||
@@ -2878,13 +2884,9 @@
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
@@ -3061,13 +3063,9 @@
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
@@ -3202,15 +3200,11 @@
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the personality for this turn and subsequent turns."
|
||||
"description": "Override the personality for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
@@ -3821,6 +3815,30 @@
|
||||
"title": "Skills/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"personalities/list"
|
||||
],
|
||||
"title": "Personalities/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PersonalitiesListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Personalities/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -602,6 +602,30 @@
|
||||
"title": "Skills/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"personalities/list"
|
||||
],
|
||||
"title": "Personalities/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/PersonalitiesListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Personalities/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -9462,11 +9486,84 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Personality": {
|
||||
"PersonalitiesListEntry": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"personalities": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PersonalityMetadata"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"personalities"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PersonalitiesListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PersonalitiesListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PersonalitiesListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/PersonalitiesListEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PersonalitiesListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PersonalityMetadata": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"isBuiltIn": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
"$ref": "#/definitions/v2/PersonalityScope"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"isBuiltIn",
|
||||
"name",
|
||||
"scope"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PersonalityScope": {
|
||||
"enum": [
|
||||
"none",
|
||||
"friendly",
|
||||
"pragmatic"
|
||||
"builtin",
|
||||
"user",
|
||||
"repo"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -13438,13 +13535,9 @@
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
@@ -13719,13 +13812,9 @@
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
@@ -14373,15 +14462,11 @@
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the personality for this turn and subsequent turns."
|
||||
"description": "Override the personality for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
|
||||
+112
-27
@@ -1177,6 +1177,30 @@
|
||||
"title": "Skills/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"personalities/list"
|
||||
],
|
||||
"title": "Personalities/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/PersonalitiesListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Personalities/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -6276,11 +6300,84 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Personality": {
|
||||
"PersonalitiesListEntry": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"personalities": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PersonalityMetadata"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"personalities"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PersonalitiesListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PersonalitiesListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"PersonalitiesListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PersonalitiesListEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PersonalitiesListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"PersonalityMetadata": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"isBuiltIn": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
"$ref": "#/definitions/PersonalityScope"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"isBuiltIn",
|
||||
"name",
|
||||
"scope"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PersonalityScope": {
|
||||
"enum": [
|
||||
"none",
|
||||
"friendly",
|
||||
"pragmatic"
|
||||
"builtin",
|
||||
"user",
|
||||
"repo"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
@@ -11284,13 +11381,9 @@
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
@@ -11565,13 +11658,9 @@
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
@@ -12219,15 +12308,11 @@
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the personality for this turn and subsequent turns."
|
||||
"description": "Override the personality for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "PersonalitiesListParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"PersonalitiesListEntry": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"personalities": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PersonalityMetadata"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"personalities"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PersonalityMetadata": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"isBuiltIn": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
"$ref": "#/definitions/PersonalityScope"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"isBuiltIn",
|
||||
"name",
|
||||
"scope"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"PersonalityScope": {
|
||||
"enum": [
|
||||
"builtin",
|
||||
"user",
|
||||
"repo"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/PersonalitiesListEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "PersonalitiesListResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -315,14 +315,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
"friendly",
|
||||
"pragmatic"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReasoningItemContent": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -1043,13 +1035,9 @@
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
|
||||
@@ -79,14 +79,6 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
"friendly",
|
||||
"pragmatic"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SandboxMode": {
|
||||
"enum": [
|
||||
"read-only",
|
||||
@@ -169,13 +161,9 @@
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandbox": {
|
||||
|
||||
@@ -113,14 +113,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
"friendly",
|
||||
"pragmatic"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"ReadOnlyAccess": {
|
||||
"oneOf": [
|
||||
{
|
||||
@@ -556,15 +548,11 @@
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Override the personality for this turn and subsequent turns."
|
||||
"description": "Override the personality for this turn and subsequent turns.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,4 +2,4 @@
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Personality = "none" | "friendly" | "pragmatic";
|
||||
export type Personality = string;
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PersonalityMetadata } from "./PersonalityMetadata";
|
||||
|
||||
export type PersonalitiesListEntry = { cwd: string, personalities: Array<PersonalityMetadata>, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type PersonalitiesListParams = { cwds?: Array<AbsolutePathBuf> | null, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PersonalitiesListEntry } from "./PersonalitiesListEntry";
|
||||
|
||||
export type PersonalitiesListResponse = { data: Array<PersonalitiesListEntry>, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { PersonalityScope } from "./PersonalityScope";
|
||||
|
||||
export type PersonalityMetadata = { name: string, description: string, scope: PersonalityScope, isBuiltIn: boolean, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PersonalityScope = "builtin" | "user" | "repo";
|
||||
@@ -209,6 +209,11 @@ export type { PatchChangeKind } from "./PatchChangeKind";
|
||||
export type { PermissionGrantScope } from "./PermissionGrantScope";
|
||||
export type { PermissionsRequestApprovalParams } from "./PermissionsRequestApprovalParams";
|
||||
export type { PermissionsRequestApprovalResponse } from "./PermissionsRequestApprovalResponse";
|
||||
export type { PersonalitiesListEntry } from "./PersonalitiesListEntry";
|
||||
export type { PersonalitiesListParams } from "./PersonalitiesListParams";
|
||||
export type { PersonalitiesListResponse } from "./PersonalitiesListResponse";
|
||||
export type { PersonalityMetadata } from "./PersonalityMetadata";
|
||||
export type { PersonalityScope } from "./PersonalityScope";
|
||||
export type { PlanDeltaNotification } from "./PlanDeltaNotification";
|
||||
export type { PluginAuthPolicy } from "./PluginAuthPolicy";
|
||||
export type { PluginDetail } from "./PluginDetail";
|
||||
|
||||
@@ -321,6 +321,10 @@ client_request_definitions! {
|
||||
params: v2::SkillsListParams,
|
||||
response: v2::SkillsListResponse,
|
||||
},
|
||||
PersonalitiesList => "personalities/list" {
|
||||
params: v2::PersonalitiesListParams,
|
||||
response: v2::PersonalitiesListResponse,
|
||||
},
|
||||
PluginList => "plugin/list" {
|
||||
params: v2::PluginListParams,
|
||||
response: v2::PluginListResponse,
|
||||
|
||||
@@ -3182,6 +3182,21 @@ pub struct SkillsListResponse {
|
||||
pub data: Vec<SkillsListEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PersonalitiesListParams {
|
||||
#[ts(optional = nullable)]
|
||||
pub cwds: Option<Vec<AbsolutePathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PersonalitiesListResponse {
|
||||
pub data: Vec<PersonalitiesListEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
@@ -3327,6 +3342,34 @@ pub struct SkillsListEntry {
|
||||
pub errors: Vec<SkillErrorInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum PersonalityScope {
|
||||
Builtin,
|
||||
User,
|
||||
Repo,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PersonalityMetadata {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub scope: PersonalityScope,
|
||||
pub is_built_in: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct PersonalitiesListEntry {
|
||||
pub cwd: PathBuf,
|
||||
pub personalities: Vec<PersonalityMetadata>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -176,6 +176,7 @@ Example with notification opt-out:
|
||||
- `experimentalFeature/enablement/set` — patch the in-memory process-wide runtime feature enablement for the currently supported feature keys (`apps`, `plugins`). For each feature, precedence is: cloud requirements > --enable <feature_name> > config.toml > experimentalFeature/enablement/set (new) > code default.
|
||||
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination). This response omits built-in developer instructions; clients should either pass `settings.developer_instructions: null` when setting a mode to use Codex's built-in instructions, or provide their own instructions explicitly.
|
||||
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
|
||||
- `personalities/list` — list built-in and file-backed personalities for one or more `cwd` values.
|
||||
- `plugin/list` — list discovered plugin marketplaces and plugin state, including effective marketplace install/auth policy metadata, fail-open `marketplaceLoadErrors` entries for marketplace files that could not be parsed or loaded, and best-effort `featuredPluginIds` for the official curated marketplace. `interface.category` uses the marketplace category when present; otherwise it falls back to the plugin manifest category. Pass `forceRemoteSync: true` to refresh curated plugin state before listing (**under development; do not call from production clients yet**).
|
||||
- `plugin/read` — read one plugin by `marketplacePath` plus `pluginName`, returning marketplace info, a list-style `summary`, manifest descriptions/interface metadata, and bundled skills/apps/MCP server names. Returned plugin skills include their current `enabled` state after local config filtering. Plugin app summaries also include `needsAuth` when the server can determine connector accessibility (**under development; do not call from production clients yet**).
|
||||
- `skills/changed` — notification emitted when watched local skill files change.
|
||||
@@ -237,7 +238,7 @@ Start a fresh thread when you need a new Codex conversation.
|
||||
{ "method": "thread/started", "params": { "thread": { … } } }
|
||||
```
|
||||
|
||||
Valid `personality` values are `"friendly"`, `"pragmatic"`, and `"none"`. When `"none"` is selected, the personality placeholder is replaced with an empty string.
|
||||
Built-in `personality` values are `"friendly"`, `"pragmatic"`, and `"none"`. Clients may also pass custom personality ids discovered via `personalities/list`. Custom personalities are loaded from Markdown files in `.codex/personalities` for the active repo and `$CODEX_HOME/personalities` for the user; invalid files are logged and ignored. When `"none"` is selected, no personality instructions are injected.
|
||||
|
||||
To continue a stored session, call `thread/resume` with the `thread.id` you previously recorded. The response shape matches `thread/start`, and no additional notifications are emitted. You can also pass the same configuration overrides supported by `thread/start`, including `approvalsReviewer`.
|
||||
|
||||
@@ -1185,6 +1186,31 @@ The server also emits `skills/changed` notifications when watched local skill fi
|
||||
}
|
||||
```
|
||||
|
||||
Use `personalities/list` to fetch the available built-in and file-backed personalities for one or more working directories.
|
||||
|
||||
```json
|
||||
{ "method": "personalities/list", "id": 26, "params": {
|
||||
"cwds": ["/Users/me/project"]
|
||||
} }
|
||||
{ "id": 26, "result": { "data": [{
|
||||
"cwd": "/Users/me/project",
|
||||
"personalities": [
|
||||
{
|
||||
"name": "friendly",
|
||||
"description": "Warm, collaborative, and helpful.",
|
||||
"scope": "builtin",
|
||||
"isBuiltIn": true
|
||||
},
|
||||
{
|
||||
"name": "night-owl",
|
||||
"description": "Late-night, direct, and fast.",
|
||||
"scope": "repo",
|
||||
"isBuiltIn": false
|
||||
}
|
||||
]
|
||||
}] } }
|
||||
```
|
||||
|
||||
To enable or disable a skill by absolute path:
|
||||
|
||||
```json
|
||||
|
||||
@@ -83,6 +83,8 @@ use codex_app_server_protocol::MockExperimentalMethodParams;
|
||||
use codex_app_server_protocol::MockExperimentalMethodResponse;
|
||||
use codex_app_server_protocol::ModelListParams;
|
||||
use codex_app_server_protocol::ModelListResponse;
|
||||
use codex_app_server_protocol::PersonalitiesListParams;
|
||||
use codex_app_server_protocol::PersonalitiesListResponse;
|
||||
use codex_app_server_protocol::PluginDetail;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
@@ -779,6 +781,10 @@ impl CodexMessageProcessor {
|
||||
self.skills_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::PersonalitiesList { request_id, params } => {
|
||||
self.personalities_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::PluginList { request_id, params } => {
|
||||
self.plugin_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -5787,6 +5793,61 @@ impl CodexMessageProcessor {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn personalities_list(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: PersonalitiesListParams,
|
||||
) {
|
||||
let cwds = params
|
||||
.cwds
|
||||
.map(|cwds| {
|
||||
cwds.into_iter()
|
||||
.map(codex_utils_absolute_path::AbsolutePathBuf::into_path_buf)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|cwds| !cwds.is_empty())
|
||||
.unwrap_or_else(|| vec![self.config.cwd.to_path_buf()]);
|
||||
|
||||
let cli_overrides = self.current_cli_overrides();
|
||||
let mut data = Vec::new();
|
||||
for cwd in cwds {
|
||||
let cwd_abs = match AbsolutePathBuf::try_from(cwd.as_path()) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
self.send_invalid_request_error(request_id, err.to_string())
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let config_layer_stack = match load_config_layers_state(
|
||||
&self.config.codex_home,
|
||||
Some(cwd_abs),
|
||||
&cli_overrides,
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(config_layer_stack) => config_layer_stack,
|
||||
Err(err) => {
|
||||
self.outgoing
|
||||
.send_error(request_id, config_load_error(&err))
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let catalog = codex_core::personalities::catalog_from_layer_stack(&config_layer_stack);
|
||||
data.push(codex_app_server_protocol::PersonalitiesListEntry {
|
||||
cwd,
|
||||
personalities: personalities_to_info(catalog.personalities()),
|
||||
});
|
||||
}
|
||||
|
||||
self.outgoing
|
||||
.send_response(request_id, PersonalitiesListResponse { data })
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn plugin_list(&self, request_id: ConnectionRequestId, params: PluginListParams) {
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let PluginListParams {
|
||||
@@ -6390,7 +6451,7 @@ impl CodexMessageProcessor {
|
||||
summary: params.summary,
|
||||
service_tier: params.service_tier,
|
||||
collaboration_mode,
|
||||
personality: params.personality,
|
||||
personality: params.personality.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -7986,6 +8047,32 @@ fn skills_to_info(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn personalities_to_info(
|
||||
personalities: &[codex_core::personalities::PersonalityDefinition],
|
||||
) -> Vec<codex_app_server_protocol::PersonalityMetadata> {
|
||||
personalities
|
||||
.iter()
|
||||
.map(
|
||||
|personality| codex_app_server_protocol::PersonalityMetadata {
|
||||
name: personality.name.to_string(),
|
||||
description: personality.description.clone(),
|
||||
scope: match personality.scope {
|
||||
codex_core::personalities::PersonalityScope::Builtin => {
|
||||
codex_app_server_protocol::PersonalityScope::Builtin
|
||||
}
|
||||
codex_core::personalities::PersonalityScope::User => {
|
||||
codex_app_server_protocol::PersonalityScope::User
|
||||
}
|
||||
codex_core::personalities::PersonalityScope::Repo => {
|
||||
codex_app_server_protocol::PersonalityScope::Repo
|
||||
}
|
||||
},
|
||||
is_built_in: personality.is_builtin,
|
||||
},
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn plugin_skills_to_info(
|
||||
skills: &[codex_core::skills::SkillMetadata],
|
||||
disabled_skill_paths: &std::collections::HashSet<PathBuf>,
|
||||
|
||||
@@ -490,6 +490,15 @@ impl McpProcess {
|
||||
self.send_request("skills/list", params).await
|
||||
}
|
||||
|
||||
/// Send a `personalities/list` JSON-RPC request.
|
||||
pub async fn send_personalities_list_request(
|
||||
&mut self,
|
||||
params: codex_app_server_protocol::PersonalitiesListParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("personalities/list", params).await
|
||||
}
|
||||
|
||||
/// Send a `plugin/install` JSON-RPC request.
|
||||
pub async fn send_plugin_install_request(
|
||||
&mut self,
|
||||
|
||||
@@ -17,6 +17,7 @@ mod initialize;
|
||||
mod mcp_server_elicitation;
|
||||
mod model_list;
|
||||
mod output_schema;
|
||||
mod personalities_list;
|
||||
mod plan_item;
|
||||
mod plugin_install;
|
||||
mod plugin_list;
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::PersonalitiesListParams;
|
||||
use codex_app_server_protocol::PersonalitiesListResponse;
|
||||
use codex_app_server_protocol::PersonalityScope;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
fn write_personality(
|
||||
root: &std::path::Path,
|
||||
name: &str,
|
||||
description: &str,
|
||||
body: &str,
|
||||
) -> Result<()> {
|
||||
std::fs::create_dir_all(root)?;
|
||||
std::fs::write(
|
||||
root.join(format!("{name}.md")),
|
||||
format!("---\nname: {name}\ndescription: {description}\n---\n\n{body}\n"),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn personalities_list_returns_builtin_and_file_backed_personalities() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let repo = TempDir::new()?;
|
||||
let repo_personalities = repo.path().join(".codex/personalities");
|
||||
let user_personalities = codex_home.path().join("personalities");
|
||||
|
||||
write_personality(
|
||||
&user_personalities,
|
||||
"night-owl",
|
||||
"User personality",
|
||||
"User instructions",
|
||||
)?;
|
||||
write_personality(
|
||||
&repo_personalities,
|
||||
"night-owl",
|
||||
"Repo personality",
|
||||
"Repo instructions",
|
||||
)?;
|
||||
write_personality(
|
||||
&repo_personalities,
|
||||
"ship-it",
|
||||
"Repo only",
|
||||
"Ship it instructions",
|
||||
)?;
|
||||
std::fs::write(
|
||||
repo_personalities.join("broken.md"),
|
||||
"# missing frontmatter\n",
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let request_id = mcp
|
||||
.send_personalities_list_request(PersonalitiesListParams {
|
||||
cwds: Some(vec![repo.path().to_path_buf().try_into()?]),
|
||||
})
|
||||
.await?;
|
||||
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
let PersonalitiesListResponse { data } = to_response(response)?;
|
||||
|
||||
assert_eq!(data.len(), 1);
|
||||
assert_eq!(data[0].cwd, repo.path().to_path_buf());
|
||||
|
||||
let personalities = &data[0].personalities;
|
||||
let names = personalities
|
||||
.iter()
|
||||
.map(|personality| personality.name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
names,
|
||||
vec!["friendly", "none", "pragmatic", "night-owl", "ship-it"]
|
||||
);
|
||||
assert!(personalities.iter().any(|personality| {
|
||||
personality.name == "friendly"
|
||||
&& personality.is_built_in
|
||||
&& personality.scope == PersonalityScope::Builtin
|
||||
}));
|
||||
assert!(personalities.iter().any(|personality| {
|
||||
personality.name == "pragmatic"
|
||||
&& personality.is_built_in
|
||||
&& personality.scope == PersonalityScope::Builtin
|
||||
}));
|
||||
assert!(personalities.iter().any(|personality| {
|
||||
personality.name == "none"
|
||||
&& personality.is_built_in
|
||||
&& personality.scope == PersonalityScope::Builtin
|
||||
}));
|
||||
|
||||
let night_owl = personalities
|
||||
.iter()
|
||||
.find(|personality| personality.name == "night-owl")
|
||||
.ok_or_else(|| anyhow!("night-owl personality missing"))?;
|
||||
assert_eq!(night_owl.description, "Repo personality");
|
||||
assert_eq!(night_owl.scope, PersonalityScope::Repo);
|
||||
assert!(!night_owl.is_built_in);
|
||||
|
||||
let ship_it = personalities
|
||||
.iter()
|
||||
.find(|personality| personality.name == "ship-it")
|
||||
.ok_or_else(|| anyhow!("ship-it personality missing"))?;
|
||||
assert_eq!(ship_it.description, "Repo only");
|
||||
assert_eq!(ship_it.scope, PersonalityScope::Repo);
|
||||
assert!(!ship_it.is_built_in);
|
||||
|
||||
assert!(
|
||||
personalities
|
||||
.iter()
|
||||
.all(|personality| personality.name != "broken")
|
||||
);
|
||||
assert_eq!(
|
||||
personalities
|
||||
.iter()
|
||||
.filter(|personality| personality.name == "night-owl")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1809,7 +1809,7 @@ async fn thread_resume_accepts_personality_override() -> Result<()> {
|
||||
.send_thread_resume_request(ThreadResumeParams {
|
||||
thread_id: thread.id,
|
||||
model: Some("gpt-5.2-codex".to_string()),
|
||||
personality: Some(Personality::Friendly),
|
||||
personality: Some(Personality::friendly()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -719,7 +719,7 @@ async fn turn_start_accepts_personality_override_v2() -> Result<()> {
|
||||
text: "Hello".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
personality: Some(Personality::Friendly),
|
||||
personality: Some(Personality::friendly()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -824,7 +824,7 @@ async fn turn_start_change_personality_mid_thread_v2() -> Result<()> {
|
||||
text: "Hello again".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
personality: Some(Personality::Friendly),
|
||||
personality: Some(Personality::friendly()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -897,7 +897,7 @@ async fn turn_start_uses_migrated_pragmatic_personality_without_override_v2() ->
|
||||
let persisted_toml: ConfigToml = toml::from_str(&std::fs::read_to_string(
|
||||
codex_home.path().join("config.toml"),
|
||||
)?)?;
|
||||
assert_eq!(persisted_toml.personality, Some(Personality::Pragmatic));
|
||||
assert_eq!(persisted_toml.personality, Some(Personality::pragmatic()));
|
||||
assert!(
|
||||
codex_home
|
||||
.path()
|
||||
|
||||
@@ -93,6 +93,7 @@ rmcp = { workspace = true, default-features = false, features = [
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
sha1 = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
similar = { workspace = true }
|
||||
|
||||
@@ -567,7 +567,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"personality": {
|
||||
"$ref": "#/definitions/Personality"
|
||||
"type": "string"
|
||||
},
|
||||
"plan_mode_reasoning_effort": {
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
@@ -1290,14 +1290,6 @@
|
||||
"PermissionsToml": {
|
||||
"type": "object"
|
||||
},
|
||||
"Personality": {
|
||||
"enum": [
|
||||
"none",
|
||||
"friendly",
|
||||
"pragmatic"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PluginConfig": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -2431,12 +2423,8 @@
|
||||
"description": "Named permissions profiles."
|
||||
},
|
||||
"personality": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/Personality"
|
||||
}
|
||||
],
|
||||
"description": "Optionally specify a personality for the model"
|
||||
"description": "Optionally specify a personality for the model",
|
||||
"type": "string"
|
||||
},
|
||||
"plan_mode_reasoning_effort": {
|
||||
"$ref": "#/definitions/ReasoningEffort"
|
||||
|
||||
+20
-14
@@ -33,6 +33,7 @@ use crate::models_manager::manager::RefreshStrategy;
|
||||
use crate::parse_command::parse_command;
|
||||
use crate::parse_turn_item;
|
||||
use crate::path_utils::normalize_for_native_workdir;
|
||||
use crate::personalities::catalog_for_config;
|
||||
use crate::realtime_conversation::RealtimeConversationManager;
|
||||
use crate::realtime_conversation::handle_audio as handle_realtime_conversation_audio;
|
||||
use crate::realtime_conversation::handle_close as handle_realtime_conversation_close;
|
||||
@@ -572,7 +573,7 @@ impl Codex {
|
||||
.base_instructions
|
||||
.clone()
|
||||
.or_else(|| conversation_history.get_base_instructions().map(|s| s.text))
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality));
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality.clone()));
|
||||
|
||||
// Respect thread-start tools. When missing (resumed/forked threads), read from the db
|
||||
// first, then fall back to rollout-file tools.
|
||||
@@ -618,7 +619,7 @@ impl Codex {
|
||||
service_tier: config.service_tier,
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
user_instructions,
|
||||
personality: config.personality,
|
||||
personality: config.personality.clone(),
|
||||
base_instructions,
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
@@ -970,7 +971,7 @@ impl TurnContext {
|
||||
compact_prompt: self.compact_prompt.clone(),
|
||||
user_instructions: self.user_instructions.clone(),
|
||||
collaboration_mode,
|
||||
personality: self.personality,
|
||||
personality: self.personality.clone(),
|
||||
approval_policy: self.approval_policy.clone(),
|
||||
sandbox_policy: self.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: self.file_system_sandbox_policy.clone(),
|
||||
@@ -1017,7 +1018,7 @@ impl TurnContext {
|
||||
sandbox_policy: self.sandbox_policy.get().clone(),
|
||||
network: self.turn_context_network_item(),
|
||||
model: self.model_info.slug.clone(),
|
||||
personality: self.personality,
|
||||
personality: self.personality.clone(),
|
||||
collaboration_mode: Some(self.collaboration_mode.clone()),
|
||||
realtime_active: Some(self.realtime_active),
|
||||
effort: self.reasoning_effort,
|
||||
@@ -1133,7 +1134,7 @@ impl SessionConfiguration {
|
||||
cwd: self.cwd.to_path_buf(),
|
||||
ephemeral: self.original_config_do_not_use.ephemeral,
|
||||
reasoning_effort: self.collaboration_mode.reasoning_effort(),
|
||||
personality: self.personality,
|
||||
personality: self.personality.clone(),
|
||||
session_source: self.session_source.clone(),
|
||||
}
|
||||
}
|
||||
@@ -1154,7 +1155,7 @@ impl SessionConfiguration {
|
||||
if let Some(service_tier) = updates.service_tier {
|
||||
next_configuration.service_tier = service_tier;
|
||||
}
|
||||
if let Some(personality) = updates.personality {
|
||||
if let Some(personality) = updates.personality.clone() {
|
||||
next_configuration.personality = Some(personality);
|
||||
}
|
||||
if let Some(approval_policy) = updates.approval_policy {
|
||||
@@ -1297,7 +1298,7 @@ impl Session {
|
||||
session_configuration.collaboration_mode.reasoning_effort();
|
||||
per_turn_config.model_reasoning_summary = session_configuration.model_reasoning_summary;
|
||||
per_turn_config.service_tier = session_configuration.service_tier;
|
||||
per_turn_config.personality = session_configuration.personality;
|
||||
per_turn_config.personality = session_configuration.personality.clone();
|
||||
per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer;
|
||||
let resolved_web_search_mode = resolve_web_search_mode_for_turn(
|
||||
&per_turn_config.web_search_mode,
|
||||
@@ -1436,7 +1437,7 @@ impl Session {
|
||||
compact_prompt: session_configuration.compact_prompt.clone(),
|
||||
user_instructions: session_configuration.user_instructions.clone(),
|
||||
collaboration_mode: session_configuration.collaboration_mode.clone(),
|
||||
personality: session_configuration.personality,
|
||||
personality: session_configuration.personality.clone(),
|
||||
approval_policy: session_configuration.approval_policy.clone(),
|
||||
sandbox_policy: session_configuration.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: session_configuration.file_system_sandbox_policy.clone(),
|
||||
@@ -1884,6 +1885,7 @@ impl Session {
|
||||
auth_manager: Arc::clone(&auth_manager),
|
||||
session_telemetry,
|
||||
models_manager: Arc::clone(&models_manager),
|
||||
personality_catalog: Arc::new(catalog_for_config(config.as_ref())),
|
||||
tool_approvals: Mutex::new(ApprovalStore::default()),
|
||||
skills_manager,
|
||||
plugins_manager: Arc::clone(&plugins_manager),
|
||||
@@ -2628,6 +2630,7 @@ impl Session {
|
||||
current_context,
|
||||
shell.as_ref(),
|
||||
exec_policy.as_ref(),
|
||||
self.services.personality_catalog.as_ref(),
|
||||
self.features.enabled(Feature::Personality),
|
||||
)
|
||||
}
|
||||
@@ -3613,16 +3616,19 @@ impl Session {
|
||||
developer_sections.push(realtime_update.into_text());
|
||||
}
|
||||
if self.features.enabled(Feature::Personality)
|
||||
&& let Some(personality) = turn_context.personality
|
||||
&& let Some(personality) = turn_context.personality.clone()
|
||||
{
|
||||
let model_info = turn_context.model_info.clone();
|
||||
let has_baked_personality = model_info.supports_personality()
|
||||
&& base_instructions == model_info.get_model_instructions(Some(personality));
|
||||
let has_baked_personality = personality.is_builtin()
|
||||
&& model_info.supports_personality()
|
||||
&& base_instructions
|
||||
== model_info.get_model_instructions(Some(personality.clone()));
|
||||
if !has_baked_personality
|
||||
&& let Some(personality_message) =
|
||||
crate::context_manager::updates::personality_message_for(
|
||||
&model_info,
|
||||
personality,
|
||||
&self.services.personality_catalog,
|
||||
&personality,
|
||||
)
|
||||
{
|
||||
developer_sections.push(
|
||||
@@ -5541,7 +5547,7 @@ async fn spawn_review_thread(
|
||||
user_instructions: None,
|
||||
compact_prompt: parent_turn_context.compact_prompt.clone(),
|
||||
collaboration_mode: parent_turn_context.collaboration_mode.clone(),
|
||||
personality: parent_turn_context.personality,
|
||||
personality: parent_turn_context.personality.clone(),
|
||||
approval_policy: parent_turn_context.approval_policy.clone(),
|
||||
sandbox_policy: parent_turn_context.sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: parent_turn_context.file_system_sandbox_policy.clone(),
|
||||
@@ -6418,7 +6424,7 @@ pub(crate) fn build_prompt(
|
||||
tools,
|
||||
parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls,
|
||||
base_instructions,
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
output_schema: turn_context.final_output_json_schema.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ async fn record_initial_history_resumed_bare_turn_context_does_not_hydrate_previ
|
||||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
network: None,
|
||||
model: previous_model.to_string(),
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
@@ -108,7 +108,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
network: None,
|
||||
model: previous_model.to_string(),
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
@@ -858,7 +858,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
network: None,
|
||||
model: previous_model.to_string(),
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
@@ -930,7 +930,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
network: None,
|
||||
model: previous_model.to_string(),
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
@@ -959,7 +959,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
network: None,
|
||||
model: previous_model.to_string(),
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
@@ -1065,7 +1065,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
network: None,
|
||||
model: current_model.to_string(),
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
@@ -1167,7 +1167,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
network: None,
|
||||
model: previous_model.to_string(),
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
@@ -1311,7 +1311,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
network: None,
|
||||
model: previous_model.to_string(),
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
|
||||
@@ -1268,7 +1268,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
||||
sandbox_policy: turn_context.sandbox_policy.get().clone(),
|
||||
network: None,
|
||||
model: previous_model.to_string(),
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
collaboration_mode: Some(turn_context.collaboration_mode.clone()),
|
||||
realtime_active: Some(turn_context.realtime_active),
|
||||
effort: turn_context.reasoning_effort,
|
||||
@@ -1801,11 +1801,11 @@ async fn set_rate_limits_retains_previous_credits() {
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
personality: config.personality.clone(),
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality.clone())),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -1899,11 +1899,11 @@ async fn set_rate_limits_updates_plan_type_when_present() {
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
personality: config.personality.clone(),
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality.clone())),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -2243,11 +2243,11 @@ pub(crate) async fn make_session_configuration_for_tests() -> SessionConfigurati
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
personality: config.personality.clone(),
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality.clone())),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -2505,11 +2505,11 @@ async fn session_new_fails_when_zsh_fork_enabled_without_zsh_path() {
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
personality: config.personality.clone(),
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality.clone())),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -2602,11 +2602,11 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
personality: config.personality.clone(),
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality.clone())),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -2682,6 +2682,7 @@ pub(crate) async fn make_session_and_context() -> (Session, TurnContext) {
|
||||
auth_manager: auth_manager.clone(),
|
||||
session_telemetry: session_telemetry.clone(),
|
||||
models_manager: Arc::clone(&models_manager),
|
||||
personality_catalog: Arc::new(crate::personalities::catalog_for_config(config.as_ref())),
|
||||
tool_approvals: Mutex::new(ApprovalStore::default()),
|
||||
skills_manager,
|
||||
plugins_manager,
|
||||
@@ -3104,7 +3105,7 @@ async fn user_turn_updates_approvals_reviewer() {
|
||||
service_tier: None,
|
||||
final_output_json_schema: None,
|
||||
collaboration_mode: None,
|
||||
personality: config.personality,
|
||||
personality: config.personality.clone(),
|
||||
},
|
||||
)
|
||||
.await;
|
||||
@@ -3445,11 +3446,11 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
developer_instructions: config.developer_instructions.clone(),
|
||||
user_instructions: config.user_instructions.clone(),
|
||||
service_tier: None,
|
||||
personality: config.personality,
|
||||
personality: config.personality.clone(),
|
||||
base_instructions: config
|
||||
.base_instructions
|
||||
.clone()
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality)),
|
||||
.unwrap_or_else(|| model_info.get_model_instructions(config.personality.clone())),
|
||||
compact_prompt: config.compact_prompt.clone(),
|
||||
approval_policy: config.permissions.approval_policy.clone(),
|
||||
approvals_reviewer: config.approvals_reviewer,
|
||||
@@ -3525,6 +3526,7 @@ pub(crate) async fn make_session_and_context_with_dynamic_tools_and_rx(
|
||||
auth_manager: Arc::clone(&auth_manager),
|
||||
session_telemetry: session_telemetry.clone(),
|
||||
models_manager: Arc::clone(&models_manager),
|
||||
personality_catalog: Arc::new(crate::personalities::catalog_for_config(config.as_ref())),
|
||||
tool_approvals: Mutex::new(ApprovalStore::default()),
|
||||
skills_manager,
|
||||
plugins_manager,
|
||||
@@ -4944,13 +4946,16 @@ async fn sample_rollout(
|
||||
&& content.iter().any(|c| {
|
||||
matches!(c, ContentItem::InputText { text } if text.contains("<personality_spec>"))
|
||||
}))
|
||||
}) && let Some(p) = reconstruction_turn.personality
|
||||
}) && let Some(p) = reconstruction_turn.personality.as_ref()
|
||||
&& session.features.enabled(Feature::Personality)
|
||||
&& let Some(personality_message) = reconstruction_turn
|
||||
.model_info
|
||||
.model_messages
|
||||
.as_ref()
|
||||
.and_then(|m| m.get_personality_message(Some(p)).filter(|s| !s.is_empty()))
|
||||
.and_then(|m| {
|
||||
m.get_personality_message(Some(p.clone()))
|
||||
.filter(|s| !s.is_empty())
|
||||
})
|
||||
{
|
||||
let msg = DeveloperInstructions::personality_spec_message(personality_message).into();
|
||||
let insert_at = initial_context
|
||||
|
||||
@@ -122,7 +122,7 @@ async fn run_compact_task_inner(
|
||||
let prompt = Prompt {
|
||||
input: turn_input,
|
||||
base_instructions: sess.get_base_instructions().await,
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
let turn_metadata_header = turn_context.turn_metadata_state.current_header_value();
|
||||
|
||||
@@ -110,7 +110,7 @@ async fn run_remote_compact_task_inner_impl(
|
||||
tools: tool_router.model_visible_specs(),
|
||||
parallel_tool_calls: turn_context.model_info.supports_parallel_tool_calls,
|
||||
base_instructions,
|
||||
personality: turn_context.personality,
|
||||
personality: turn_context.personality.clone(),
|
||||
output_schema: None,
|
||||
};
|
||||
|
||||
|
||||
@@ -4457,7 +4457,7 @@ fn test_precedence_fixture_with_o3_profile() -> std::io::Result<()> {
|
||||
model_supports_reasoning_summaries: None,
|
||||
model_catalog: None,
|
||||
model_verbosity: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
personality: Some(Personality::pragmatic()),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
@@ -4599,7 +4599,7 @@ fn test_precedence_fixture_with_gpt3_profile() -> std::io::Result<()> {
|
||||
model_supports_reasoning_summaries: None,
|
||||
model_catalog: None,
|
||||
model_verbosity: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
personality: Some(Personality::pragmatic()),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
@@ -4739,7 +4739,7 @@ fn test_precedence_fixture_with_zdr_profile() -> std::io::Result<()> {
|
||||
model_supports_reasoning_summaries: None,
|
||||
model_catalog: None,
|
||||
model_verbosity: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
personality: Some(Personality::pragmatic()),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
@@ -4865,7 +4865,7 @@ fn test_precedence_fixture_with_gpt5_profile() -> std::io::Result<()> {
|
||||
model_supports_reasoning_summaries: None,
|
||||
model_catalog: None,
|
||||
model_verbosity: Some(Verbosity::High),
|
||||
personality: Some(Personality::Pragmatic),
|
||||
personality: Some(Personality::pragmatic()),
|
||||
chatgpt_base_url: "https://chatgpt.com/backend-api/".to_string(),
|
||||
realtime_audio: RealtimeAudioConfig::default(),
|
||||
experimental_realtime_start_instructions: None,
|
||||
|
||||
@@ -383,7 +383,9 @@ impl ConfigDocument {
|
||||
)),
|
||||
ConfigEdit::SetModelPersonality { personality } => Ok(self.write_profile_value(
|
||||
&["personality"],
|
||||
personality.map(|personality| value(personality.to_string())),
|
||||
personality
|
||||
.clone()
|
||||
.map(|personality| value(personality.to_string())),
|
||||
)),
|
||||
ConfigEdit::SetNoticeHideFullAccessWarning(acknowledged) => Ok(self.write_value(
|
||||
Scope::Global,
|
||||
|
||||
@@ -2431,7 +2431,7 @@ impl Config {
|
||||
.or_else(|| {
|
||||
features
|
||||
.enabled(Feature::Personality)
|
||||
.then_some(Personality::Pragmatic)
|
||||
.then_some(Personality::pragmatic())
|
||||
});
|
||||
|
||||
let experimental_compact_prompt_path = config_profile
|
||||
|
||||
@@ -130,7 +130,10 @@ impl ContextManager {
|
||||
// This is a coarse lower bound, not a tokenizer-accurate count.
|
||||
pub(crate) fn estimate_token_count(&self, turn_context: &TurnContext) -> Option<i64> {
|
||||
let model_info = &turn_context.model_info;
|
||||
let personality = turn_context.personality.or(turn_context.config.personality);
|
||||
let personality = turn_context
|
||||
.personality
|
||||
.clone()
|
||||
.or(turn_context.config.personality.clone());
|
||||
let base_instructions = BaseInstructions {
|
||||
text: model_info.get_model_instructions(personality),
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::codex::PreviousTurnSettings;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::environment_context::EnvironmentContext;
|
||||
use crate::personalities::PersonalityCatalog;
|
||||
use crate::shell::Shell;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_features::Feature;
|
||||
@@ -107,6 +108,7 @@ pub(crate) fn build_initial_realtime_item(
|
||||
fn build_personality_update_item(
|
||||
previous: Option<&TurnContextItem>,
|
||||
next: &TurnContext,
|
||||
personality_catalog: &PersonalityCatalog,
|
||||
personality_feature_enabled: bool,
|
||||
) -> Option<DeveloperInstructions> {
|
||||
if !personality_feature_enabled {
|
||||
@@ -117,11 +119,12 @@ fn build_personality_update_item(
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(personality) = next.personality
|
||||
if let Some(personality) = next.personality.clone()
|
||||
&& next.personality != previous.personality
|
||||
{
|
||||
let model_info = &next.model_info;
|
||||
let personality_message = personality_message_for(model_info, personality);
|
||||
let personality_message =
|
||||
personality_message_for(model_info, personality_catalog, &personality);
|
||||
personality_message.map(DeveloperInstructions::personality_spec_message)
|
||||
} else {
|
||||
None
|
||||
@@ -130,13 +133,22 @@ fn build_personality_update_item(
|
||||
|
||||
pub(crate) fn personality_message_for(
|
||||
model_info: &ModelInfo,
|
||||
personality: Personality,
|
||||
personality_catalog: &PersonalityCatalog,
|
||||
personality: &Personality,
|
||||
) -> Option<String> {
|
||||
model_info
|
||||
let builtin_message = model_info
|
||||
.model_messages
|
||||
.as_ref()
|
||||
.and_then(|spec| spec.get_personality_message(Some(personality)))
|
||||
.filter(|message| !message.is_empty())
|
||||
.and_then(|spec| spec.get_personality_message(Some(personality.clone())))
|
||||
.filter(|message| !message.is_empty());
|
||||
if builtin_message.is_some() {
|
||||
builtin_message
|
||||
} else {
|
||||
personality_catalog
|
||||
.instructions_for(personality)
|
||||
.map(ToOwned::to_owned)
|
||||
.filter(|message| !message.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_model_instructions_update_item(
|
||||
@@ -148,7 +160,9 @@ pub(crate) fn build_model_instructions_update_item(
|
||||
return None;
|
||||
}
|
||||
|
||||
let model_instructions = next.model_info.get_model_instructions(next.personality);
|
||||
let model_instructions = next
|
||||
.model_info
|
||||
.get_model_instructions(next.personality.clone());
|
||||
if model_instructions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
@@ -191,6 +205,7 @@ pub(crate) fn build_settings_update_items(
|
||||
next: &TurnContext,
|
||||
shell: &Shell,
|
||||
exec_policy: &Policy,
|
||||
personality_catalog: &PersonalityCatalog,
|
||||
personality_feature_enabled: bool,
|
||||
) -> Vec<ResponseItem> {
|
||||
// TODO(ccunningham): build_settings_update_items still does not cover every
|
||||
@@ -205,7 +220,12 @@ pub(crate) fn build_settings_update_items(
|
||||
build_permissions_update_item(previous, next, exec_policy),
|
||||
build_collaboration_mode_update_item(previous, next),
|
||||
build_realtime_update_item(previous, previous_turn_settings, next),
|
||||
build_personality_update_item(previous, next, personality_feature_enabled),
|
||||
build_personality_update_item(
|
||||
previous,
|
||||
next,
|
||||
personality_catalog,
|
||||
personality_feature_enabled,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
|
||||
@@ -331,7 +331,7 @@ pub(super) async fn run_guardian_review_session(
|
||||
model: guardian_model,
|
||||
reasoning_effort: guardian_reasoning_effort,
|
||||
reasoning_summary: turn.reasoning_summary,
|
||||
personality: turn.personality,
|
||||
personality: turn.personality.clone(),
|
||||
external_cancel,
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -522,7 +522,7 @@ async fn run_review_on_session(
|
||||
service_tier: None,
|
||||
final_output_json_schema: Some(params.schema.clone()),
|
||||
collaboration_mode: None,
|
||||
personality: params.personality,
|
||||
personality: params.personality.clone(),
|
||||
})
|
||||
.await
|
||||
}),
|
||||
|
||||
@@ -63,6 +63,7 @@ pub mod message_history;
|
||||
mod model_provider_info;
|
||||
pub mod utils;
|
||||
pub use utils::path_utils;
|
||||
pub mod personalities;
|
||||
pub mod personality_migration;
|
||||
pub mod plugins;
|
||||
mod provider_auth;
|
||||
|
||||
@@ -0,0 +1,487 @@
|
||||
/*!
|
||||
Loads built-in and file-backed personality definitions, resolves precedence
|
||||
between repo and user scopes, and exposes the catalog used by config, prompt
|
||||
construction, and the app-server personality picker.
|
||||
*/
|
||||
|
||||
use crate::config::Config;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tracing::warn;
|
||||
|
||||
const PERSONALITIES_DIR_NAME: &str = "personalities";
|
||||
const MAX_NAME_LEN: usize = 64;
|
||||
const MAX_DESCRIPTION_LEN: usize = 1024;
|
||||
const BUILTIN_FRIENDLY_INSTRUCTIONS: &str =
|
||||
include_str!("../templates/personalities/gpt-5.2-codex_friendly.md");
|
||||
const BUILTIN_PRAGMATIC_INSTRUCTIONS: &str =
|
||||
include_str!("../templates/personalities/gpt-5.2-codex_pragmatic.md");
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PersonalityScope {
|
||||
Builtin,
|
||||
User,
|
||||
Repo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PersonalityDefinition {
|
||||
pub name: Personality,
|
||||
pub description: String,
|
||||
pub instructions: Option<String>,
|
||||
pub scope: PersonalityScope,
|
||||
pub is_builtin: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PersonalityCatalog {
|
||||
personalities: Vec<PersonalityDefinition>,
|
||||
by_name: HashMap<Personality, PersonalityDefinition>,
|
||||
}
|
||||
|
||||
impl PersonalityCatalog {
|
||||
pub fn personalities(&self) -> &[PersonalityDefinition] {
|
||||
self.personalities.as_slice()
|
||||
}
|
||||
|
||||
pub fn get(&self, personality: &Personality) -> Option<&PersonalityDefinition> {
|
||||
self.by_name.get(personality)
|
||||
}
|
||||
|
||||
pub fn instructions_for(&self, personality: &Personality) -> Option<&str> {
|
||||
self.get(personality)
|
||||
.and_then(|definition| definition.instructions.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct PersonalityLoadOutcome {
|
||||
personalities: Vec<PersonalityDefinition>,
|
||||
errors: Vec<PersonalityError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct PersonalityError {
|
||||
path: PathBuf,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PersonalityFrontmatter {
|
||||
name: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PersonalityParseError {
|
||||
Read(std::io::Error),
|
||||
MissingFrontmatter,
|
||||
InvalidYaml(serde_yaml::Error),
|
||||
MissingBody,
|
||||
InvalidName(String),
|
||||
InvalidField { field: &'static str, reason: String },
|
||||
}
|
||||
|
||||
impl fmt::Display for PersonalityParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
PersonalityParseError::Read(error) => write!(f, "failed to read file: {error}"),
|
||||
PersonalityParseError::MissingFrontmatter => {
|
||||
write!(f, "missing YAML frontmatter delimited by ---")
|
||||
}
|
||||
PersonalityParseError::InvalidYaml(error) => write!(f, "invalid YAML: {error}"),
|
||||
PersonalityParseError::MissingBody => write!(f, "missing markdown body"),
|
||||
PersonalityParseError::InvalidName(reason) => write!(f, "invalid name: {reason}"),
|
||||
PersonalityParseError::InvalidField { field, reason } => {
|
||||
write!(f, "invalid {field}: {reason}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for PersonalityParseError {}
|
||||
|
||||
pub fn catalog_for_config(config: &Config) -> PersonalityCatalog {
|
||||
catalog_from_layer_stack(&config.config_layer_stack)
|
||||
}
|
||||
|
||||
pub fn catalog_from_layer_stack(config_layer_stack: &ConfigLayerStack) -> PersonalityCatalog {
|
||||
let mut outcome = load_personalities_from_layer_stack(config_layer_stack);
|
||||
for error in &outcome.errors {
|
||||
warn!(
|
||||
path = %error.path.display(),
|
||||
error = %error.message,
|
||||
"ignoring invalid personality definition"
|
||||
);
|
||||
}
|
||||
|
||||
outcome.personalities.sort_by(|left, right| {
|
||||
left.is_builtin
|
||||
.cmp(&right.is_builtin)
|
||||
.reverse()
|
||||
.then_with(|| {
|
||||
left.name
|
||||
.as_str()
|
||||
.to_ascii_lowercase()
|
||||
.cmp(&right.name.as_str().to_ascii_lowercase())
|
||||
})
|
||||
.then_with(|| left.name.as_str().cmp(right.name.as_str()))
|
||||
});
|
||||
|
||||
let mut by_name = HashMap::new();
|
||||
for personality in outcome.personalities.iter().cloned() {
|
||||
by_name.insert(personality.name.clone(), personality);
|
||||
}
|
||||
PersonalityCatalog {
|
||||
personalities: outcome.personalities,
|
||||
by_name,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_personalities_from_layer_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
) -> PersonalityLoadOutcome {
|
||||
let mut outcome = PersonalityLoadOutcome {
|
||||
personalities: builtin_personalities(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut seen: HashSet<Personality> = outcome
|
||||
.personalities
|
||||
.iter()
|
||||
.map(|personality| personality.name.clone())
|
||||
.collect();
|
||||
|
||||
for (root, scope) in personality_roots(config_layer_stack) {
|
||||
discover_personalities_under_root(root.as_path(), scope, &mut seen, &mut outcome);
|
||||
}
|
||||
|
||||
outcome
|
||||
}
|
||||
|
||||
fn builtin_personalities() -> Vec<PersonalityDefinition> {
|
||||
vec![
|
||||
PersonalityDefinition {
|
||||
name: Personality::none(),
|
||||
description: "No personality instructions.".to_string(),
|
||||
instructions: Some(String::new()),
|
||||
scope: PersonalityScope::Builtin,
|
||||
is_builtin: true,
|
||||
},
|
||||
PersonalityDefinition {
|
||||
name: Personality::friendly(),
|
||||
description: "Warm, collaborative, and helpful.".to_string(),
|
||||
instructions: Some(BUILTIN_FRIENDLY_INSTRUCTIONS.to_string()),
|
||||
scope: PersonalityScope::Builtin,
|
||||
is_builtin: true,
|
||||
},
|
||||
PersonalityDefinition {
|
||||
name: Personality::pragmatic(),
|
||||
description: "Concise, task-focused, and direct.".to_string(),
|
||||
instructions: Some(BUILTIN_PRAGMATIC_INSTRUCTIONS.to_string()),
|
||||
scope: PersonalityScope::Builtin,
|
||||
is_builtin: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn personality_roots(config_layer_stack: &ConfigLayerStack) -> Vec<(PathBuf, PersonalityScope)> {
|
||||
let mut roots = Vec::new();
|
||||
for layer in config_layer_stack.get_layers(
|
||||
ConfigLayerStackOrdering::HighestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
) {
|
||||
let Some(config_folder) = layer.config_folder() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let scope = match &layer.name {
|
||||
ConfigLayerSource::Project { .. } => PersonalityScope::Repo,
|
||||
ConfigLayerSource::User { .. } => PersonalityScope::User,
|
||||
_ => continue,
|
||||
};
|
||||
roots.push((config_folder.as_path().join(PERSONALITIES_DIR_NAME), scope));
|
||||
}
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
roots.retain(|(path, _)| seen.insert(path.clone()));
|
||||
roots
|
||||
}
|
||||
|
||||
fn discover_personalities_under_root(
|
||||
root: &Path,
|
||||
scope: PersonalityScope,
|
||||
seen: &mut HashSet<Personality>,
|
||||
outcome: &mut PersonalityLoadOutcome,
|
||||
) {
|
||||
let entries = match fs::read_dir(root) {
|
||||
Ok(entries) => entries,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
|
||||
Err(error) => {
|
||||
outcome.errors.push(PersonalityError {
|
||||
path: root.to_path_buf(),
|
||||
message: error.to_string(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_file()
|
||||
|| !path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.is_some_and(|extension| extension.eq_ignore_ascii_case("md"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
match parse_personality_file(path.as_path(), scope) {
|
||||
Ok(personality) => {
|
||||
if seen.insert(personality.name.clone()) {
|
||||
outcome.personalities.push(personality);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
outcome.errors.push(PersonalityError {
|
||||
path,
|
||||
message: error.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_personality_file(
|
||||
path: &Path,
|
||||
scope: PersonalityScope,
|
||||
) -> Result<PersonalityDefinition, PersonalityParseError> {
|
||||
let contents = fs::read_to_string(path).map_err(PersonalityParseError::Read)?;
|
||||
let (frontmatter, body) =
|
||||
extract_frontmatter_and_body(&contents).ok_or(PersonalityParseError::MissingFrontmatter)?;
|
||||
let parsed: PersonalityFrontmatter =
|
||||
serde_yaml::from_str(frontmatter.as_str()).map_err(PersonalityParseError::InvalidYaml)?;
|
||||
|
||||
let name = sanitize_single_line(parsed.name.as_str());
|
||||
validate_name(name.as_str())?;
|
||||
|
||||
let description = sanitize_single_line(parsed.description.as_str());
|
||||
validate_len(description.as_str(), MAX_DESCRIPTION_LEN, "description")?;
|
||||
|
||||
let instructions = body.trim();
|
||||
if instructions.is_empty() {
|
||||
return Err(PersonalityParseError::MissingBody);
|
||||
}
|
||||
|
||||
Ok(PersonalityDefinition {
|
||||
name: Personality::from(name),
|
||||
description,
|
||||
instructions: Some(instructions.to_string()),
|
||||
scope,
|
||||
is_builtin: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_name(name: &str) -> Result<(), PersonalityParseError> {
|
||||
validate_len(name, MAX_NAME_LEN, "name")?;
|
||||
let normalized = name.to_ascii_lowercase();
|
||||
if Personality::builtin_names().contains(&normalized.as_str()) {
|
||||
return Err(PersonalityParseError::InvalidName(format!(
|
||||
"`{name}` is reserved for a built-in personality"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_len(
|
||||
value: &str,
|
||||
max_len: usize,
|
||||
field_name: &'static str,
|
||||
) -> Result<(), PersonalityParseError> {
|
||||
if value.is_empty() {
|
||||
return Err(PersonalityParseError::InvalidField {
|
||||
field: field_name,
|
||||
reason: "value is empty".to_string(),
|
||||
});
|
||||
}
|
||||
if value.chars().count() > max_len {
|
||||
return Err(PersonalityParseError::InvalidField {
|
||||
field: field_name,
|
||||
reason: format!("exceeds maximum length of {max_len} characters"),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sanitize_single_line(raw: &str) -> String {
|
||||
raw.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
|
||||
fn extract_frontmatter_and_body(contents: &str) -> Option<(String, String)> {
|
||||
let mut lines = contents.lines();
|
||||
if !matches!(lines.next(), Some(line) if line.trim() == "---") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut frontmatter_lines = Vec::new();
|
||||
let mut found_closing = false;
|
||||
for line in lines.by_ref() {
|
||||
if line.trim() == "---" {
|
||||
found_closing = true;
|
||||
break;
|
||||
}
|
||||
frontmatter_lines.push(line);
|
||||
}
|
||||
|
||||
if frontmatter_lines.is_empty() || !found_closing {
|
||||
return None;
|
||||
}
|
||||
|
||||
let body = lines.collect::<Vec<_>>().join("\n");
|
||||
Some((frontmatter_lines.join("\n"), body))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::catalog_from_layer_stack;
|
||||
use crate::config_loader::CloudRequirementsLoader;
|
||||
use crate::config_loader::LoaderOverrides;
|
||||
use crate::config_loader::load_config_layers_state;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
fn write_personality(root: &std::path::Path, name: &str, description: &str, body: &str) {
|
||||
fs::create_dir_all(root).unwrap();
|
||||
let path = root.join(format!("{name}.md"));
|
||||
fs::write(
|
||||
path,
|
||||
format!("---\nname: {name}\ndescription: {description}\n---\n\n{body}\n"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn repo_personality_overrides_user_personality_name() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let codex_home = temp.path().join(".codex-home");
|
||||
let repo_root = temp.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".codex")).unwrap();
|
||||
fs::create_dir_all(&codex_home).unwrap();
|
||||
|
||||
write_personality(
|
||||
codex_home.join("personalities").as_path(),
|
||||
"night-owl",
|
||||
"User personality",
|
||||
"user body",
|
||||
);
|
||||
write_personality(
|
||||
repo_root.join(".codex/personalities").as_path(),
|
||||
"night-owl",
|
||||
"Repo personality",
|
||||
"repo body",
|
||||
);
|
||||
|
||||
let stack = load_config_layers_state(
|
||||
&codex_home,
|
||||
Some(repo_root.clone().try_into().unwrap()),
|
||||
&[],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let catalog = catalog_from_layer_stack(&stack);
|
||||
let personality = catalog.get(&Personality::from("night-owl")).unwrap();
|
||||
assert_eq!(personality.description, "Repo personality");
|
||||
assert_eq!(personality.instructions.as_deref(), Some("repo body"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_personality_is_ignored() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let codex_home = temp.path().join(".codex-home");
|
||||
let repo_root = temp.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".codex/personalities")).unwrap();
|
||||
fs::create_dir_all(&codex_home).unwrap();
|
||||
fs::write(
|
||||
repo_root.join(".codex/personalities/bad.md"),
|
||||
"---\nname: friendly\ndescription: bad\n---\n\nbody\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let stack = load_config_layers_state(
|
||||
&codex_home,
|
||||
Some(repo_root.clone().try_into().unwrap()),
|
||||
&[],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let catalog = catalog_from_layer_stack(&stack);
|
||||
assert!(
|
||||
catalog
|
||||
.get(&Personality::from("friendly"))
|
||||
.unwrap()
|
||||
.is_builtin
|
||||
);
|
||||
assert!(catalog.get(&Personality::from("bad")).is_none());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn personalities_are_sorted_with_builtins_first_then_name() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let codex_home = temp.path().join(".codex-home");
|
||||
let repo_root = temp.path().join("repo");
|
||||
fs::create_dir_all(repo_root.join(".codex")).unwrap();
|
||||
fs::create_dir_all(&codex_home).unwrap();
|
||||
|
||||
write_personality(
|
||||
codex_home.join("personalities").as_path(),
|
||||
"zebra",
|
||||
"User personality",
|
||||
"zebra body",
|
||||
);
|
||||
write_personality(
|
||||
repo_root.join(".codex/personalities").as_path(),
|
||||
"alpha",
|
||||
"Repo personality",
|
||||
"alpha body",
|
||||
);
|
||||
|
||||
let stack = load_config_layers_state(
|
||||
&codex_home,
|
||||
Some(repo_root.clone().try_into().unwrap()),
|
||||
&[],
|
||||
LoaderOverrides::default(),
|
||||
CloudRequirementsLoader::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let catalog = catalog_from_layer_stack(&stack);
|
||||
let names = catalog
|
||||
.personalities()
|
||||
.iter()
|
||||
.map(|personality| personality.name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
names,
|
||||
vec!["friendly", "none", "pragmatic", "alpha", "zebra"]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ pub async fn maybe_migrate_personality(
|
||||
}
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.set_personality(Some(Personality::Pragmatic))
|
||||
.set_personality(Some(Personality::pragmatic()))
|
||||
.apply()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
||||
@@ -81,7 +81,7 @@ async fn applies_when_sessions_exist_and_no_personality() -> io::Result<()> {
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
assert_eq!(persisted.personality, Some(Personality::pragmatic()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ async fn skips_when_marker_exists() -> io::Result<()> {
|
||||
async fn skips_when_personality_explicit() -> io::Result<()> {
|
||||
let temp = TempDir::new()?;
|
||||
ConfigEditsBuilder::new(temp.path())
|
||||
.set_personality(Some(Personality::Friendly))
|
||||
.set_personality(Some(Personality::friendly()))
|
||||
.apply()
|
||||
.await
|
||||
.map_err(|err| io::Error::other(format!("failed to write config: {err}")))?;
|
||||
@@ -117,7 +117,7 @@ async fn skips_when_personality_explicit() -> io::Result<()> {
|
||||
assert!(temp.path().join(PERSONALITY_MIGRATION_FILENAME).exists());
|
||||
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(persisted.personality, Some(Personality::Friendly));
|
||||
assert_eq!(persisted.personality, Some(Personality::friendly()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::exec_policy::ExecPolicyManager;
|
||||
use crate::mcp::McpManager;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::models_manager::manager::ModelsManager;
|
||||
use crate::personalities::PersonalityCatalog;
|
||||
use crate::plugins::PluginsManager;
|
||||
use crate::skills_watcher::SkillsWatcher;
|
||||
use crate::state_db::StateDbHandle;
|
||||
@@ -44,6 +45,7 @@ pub(crate) struct SessionServices {
|
||||
pub(crate) exec_policy: Arc<ExecPolicyManager>,
|
||||
pub(crate) auth_manager: Arc<AuthManager>,
|
||||
pub(crate) models_manager: Arc<ModelsManager>,
|
||||
pub(crate) personality_catalog: Arc<PersonalityCatalog>,
|
||||
pub(crate) session_telemetry: SessionTelemetry,
|
||||
pub(crate) tool_approvals: Mutex<ApprovalStore>,
|
||||
pub(crate) skills_manager: Arc<SkillsManager>,
|
||||
|
||||
@@ -247,7 +247,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
personality: Some(Personality::pragmatic()),
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
|
||||
.features
|
||||
.enable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::Pragmatic);
|
||||
config.personality = Some(Personality::pragmatic());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
let preturn_context_diff_cwd = test.cwd_path().join(PRETURN_CONTEXT_DIFF_CWD);
|
||||
@@ -152,7 +152,7 @@ async fn snapshot_model_visible_layout_turn_overrides() -> Result<()> {
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: Some(Personality::Friendly),
|
||||
personality: Some(Personality::friendly()),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
@@ -343,7 +343,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
|
||||
.features
|
||||
.enable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::Pragmatic);
|
||||
config.personality = Some(Personality::pragmatic());
|
||||
});
|
||||
let resumed = resume_builder.resume(&server, home, rollout_path).await?;
|
||||
let resume_override_cwd = resumed.cwd_path().join(PRETURN_CONTEXT_DIFF_CWD);
|
||||
@@ -365,7 +365,7 @@ async fn snapshot_model_visible_layout_resume_with_personality_change() -> Resul
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: Some(Personality::Friendly),
|
||||
personality: Some(Personality::friendly()),
|
||||
})
|
||||
.await?;
|
||||
wait_for_event(&resumed.codex, |event| {
|
||||
|
||||
@@ -48,7 +48,7 @@ async fn personality_does_not_mutate_base_instructions_without_template() {
|
||||
.features
|
||||
.enable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::Friendly);
|
||||
config.personality = Some(Personality::friendly());
|
||||
|
||||
let model_info = codex_core::test_support::construct_model_info_offline("gpt-5.1", &config);
|
||||
assert_eq!(
|
||||
@@ -65,7 +65,7 @@ async fn base_instructions_override_disables_personality_template() {
|
||||
.features
|
||||
.enable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::Friendly);
|
||||
config.personality = Some(Personality::friendly());
|
||||
config.base_instructions = Some("override instructions".to_string());
|
||||
|
||||
let model_info =
|
||||
@@ -141,7 +141,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result<
|
||||
.features
|
||||
.enable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::Friendly);
|
||||
config.personality = Some(Personality::friendly());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -199,7 +199,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> {
|
||||
.features
|
||||
.enable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::None);
|
||||
config.personality = Some(Personality::none());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -353,7 +353,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()>
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: Some(Personality::Friendly),
|
||||
personality: Some(Personality::friendly()),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -420,7 +420,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
|
||||
.features
|
||||
.enable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::Pragmatic);
|
||||
config.personality = Some(Personality::pragmatic());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -458,7 +458,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
personality: Some(Personality::pragmatic()),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -510,7 +510,7 @@ async fn instructions_uses_base_if_feature_disabled() -> anyhow::Result<()> {
|
||||
.features
|
||||
.disable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.personality = Some(Personality::Friendly);
|
||||
config.personality = Some(Personality::friendly());
|
||||
|
||||
let model_info =
|
||||
codex_core::test_support::construct_model_info_offline("gpt-5.2-codex", &config);
|
||||
@@ -576,7 +576,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()>
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: Some(Personality::Pragmatic),
|
||||
personality: Some(Personality::pragmatic()),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -691,7 +691,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
|
||||
.enable(Feature::Personality)
|
||||
.expect("test config should allow feature update");
|
||||
config.model = Some(remote_slug.to_string());
|
||||
config.personality = Some(Personality::Friendly);
|
||||
config.personality = Some(Personality::friendly());
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
@@ -713,7 +713,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: Some(Personality::Friendly),
|
||||
personality: Some(Personality::friendly()),
|
||||
})
|
||||
.await?;
|
||||
|
||||
@@ -850,7 +850,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() -
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: Some(Personality::Friendly),
|
||||
personality: Some(Personality::friendly()),
|
||||
})
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ async fn no_marker_sessions_sets_personality() -> io::Result<()> {
|
||||
);
|
||||
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
assert_eq!(persisted.personality, Some(Personality::pragmatic()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ async fn no_marker_sessions_preserves_existing_config_fields() -> io::Result<()>
|
||||
assert_eq!(status, PersonalityMigrationStatus::Applied);
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(persisted.model, Some("gpt-5-codex".to_string()));
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
assert_eq!(persisted.personality, Some(Personality::pragmatic()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ async fn applied_migration_is_idempotent_on_second_run() -> io::Result<()> {
|
||||
assert_eq!(first_status, PersonalityMigrationStatus::Applied);
|
||||
assert_eq!(second_status, PersonalityMigrationStatus::SkippedMarker);
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
assert_eq!(persisted.personality, Some(Personality::pragmatic()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -333,6 +333,6 @@ async fn no_marker_archived_sessions_sets_personality() -> io::Result<()> {
|
||||
);
|
||||
|
||||
let persisted = read_config_toml(temp.path()).await?;
|
||||
assert_eq!(persisted.personality, Some(Personality::Pragmatic));
|
||||
assert_eq!(persisted.personality, Some(Personality::pragmatic()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ For complete documentation of the `Op` and `EventMsg` variants, refer to [protoc
|
||||
- `Op::ListSkills` – Request skills for one or more cwd values (optionally `force_reload`)
|
||||
- `Op::UserTurn` and `Op::OverrideTurnContext` accept an optional `personality` override that updates the model’s communication style
|
||||
|
||||
Valid `personality` values are `friendly`, `pragmatic`, and `none`. When `none` is selected, the personality placeholder is replaced with an empty string.
|
||||
Built-in `personality` values are `friendly`, `pragmatic`, and `none`. Newer clients may also use custom personality ids backed by `.codex/personalities` or `$CODEX_HOME/personalities`; older servers reject unknown values. When `none` is selected, no personality instructions are injected.
|
||||
|
||||
- `EventMsg`
|
||||
- `EventMsg::AgentMessage` – Messages from the `Model`
|
||||
|
||||
@@ -2,10 +2,10 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
use std::num::NonZeroU64;
|
||||
use std::time::Duration;
|
||||
use strum_macros::Display;
|
||||
use strum_macros::EnumIter;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::openai_models::ReasoningEffort;
|
||||
@@ -98,26 +98,80 @@ pub enum WindowsSandboxLevel {
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Clone,
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Display,
|
||||
JsonSchema,
|
||||
TS,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
EnumIter,
|
||||
Debug, Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, TS, PartialOrd, Ord, Hash,
|
||||
)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[strum(serialize_all = "lowercase")]
|
||||
pub enum Personality {
|
||||
None,
|
||||
Friendly,
|
||||
Pragmatic,
|
||||
#[serde(transparent)]
|
||||
#[ts(type = "string")]
|
||||
pub struct Personality(String);
|
||||
|
||||
impl Personality {
|
||||
pub const NONE: &str = "none";
|
||||
pub const FRIENDLY: &str = "friendly";
|
||||
pub const PRAGMATIC: &str = "pragmatic";
|
||||
|
||||
pub fn new(value: impl Into<String>) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
pub fn is_builtin(&self) -> bool {
|
||||
self.is_none() || self.is_friendly() || self.is_pragmatic()
|
||||
}
|
||||
|
||||
pub fn is_none(&self) -> bool {
|
||||
self.as_str() == Self::NONE
|
||||
}
|
||||
|
||||
pub fn is_friendly(&self) -> bool {
|
||||
self.as_str() == Self::FRIENDLY
|
||||
}
|
||||
|
||||
pub fn is_pragmatic(&self) -> bool {
|
||||
self.as_str() == Self::PRAGMATIC
|
||||
}
|
||||
|
||||
pub fn builtin_names() -> [&'static str; 3] {
|
||||
[Self::NONE, Self::FRIENDLY, Self::PRAGMATIC]
|
||||
}
|
||||
|
||||
pub fn none() -> Self {
|
||||
Self::new(Self::NONE)
|
||||
}
|
||||
|
||||
pub fn friendly() -> Self {
|
||||
Self::new(Self::FRIENDLY)
|
||||
}
|
||||
|
||||
pub fn pragmatic() -> Self {
|
||||
Self::new(Self::PRAGMATIC)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Personality {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Personality {
|
||||
fn from(value: String) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Personality> for String {
|
||||
fn from(value: Personality) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Personality {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
||||
@@ -382,10 +382,14 @@ impl ModelInstructionsVariables {
|
||||
|
||||
pub fn get_personality_message(&self, personality: Option<Personality>) -> Option<String> {
|
||||
if let Some(personality) = personality {
|
||||
match personality {
|
||||
Personality::None => Some(String::new()),
|
||||
Personality::Friendly => self.personality_friendly.clone(),
|
||||
Personality::Pragmatic => self.personality_pragmatic.clone(),
|
||||
if personality.is_none() {
|
||||
Some(String::new())
|
||||
} else if personality.is_friendly() {
|
||||
self.personality_friendly.clone()
|
||||
} else if personality.is_pragmatic() {
|
||||
self.personality_pragmatic.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
self.personality_default.clone()
|
||||
@@ -579,7 +583,7 @@ mod tests {
|
||||
instructions_variables: Some(personality_variables()),
|
||||
}));
|
||||
|
||||
let instructions = model.get_model_instructions(Some(Personality::Friendly));
|
||||
let instructions = model.get_model_instructions(Some(Personality::friendly()));
|
||||
|
||||
assert_eq!(instructions, "Hello friendly");
|
||||
}
|
||||
@@ -595,15 +599,15 @@ mod tests {
|
||||
}),
|
||||
}));
|
||||
assert_eq!(
|
||||
model.get_model_instructions(Some(Personality::Friendly)),
|
||||
model.get_model_instructions(Some(Personality::friendly())),
|
||||
"Hello\nfriendly"
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_model_instructions(Some(Personality::Pragmatic)),
|
||||
model.get_model_instructions(Some(Personality::pragmatic())),
|
||||
"Hello\n"
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_model_instructions(Some(Personality::None)),
|
||||
model.get_model_instructions(Some(Personality::none())),
|
||||
"Hello\n"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -620,15 +624,15 @@ mod tests {
|
||||
}),
|
||||
}));
|
||||
assert_eq!(
|
||||
model_no_personality.get_model_instructions(Some(Personality::Friendly)),
|
||||
model_no_personality.get_model_instructions(Some(Personality::friendly())),
|
||||
"Hello\n"
|
||||
);
|
||||
assert_eq!(
|
||||
model_no_personality.get_model_instructions(Some(Personality::Pragmatic)),
|
||||
model_no_personality.get_model_instructions(Some(Personality::pragmatic())),
|
||||
"Hello\n"
|
||||
);
|
||||
assert_eq!(
|
||||
model_no_personality.get_model_instructions(Some(Personality::None)),
|
||||
model_no_personality.get_model_instructions(Some(Personality::none())),
|
||||
"Hello\n"
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -648,7 +652,7 @@ mod tests {
|
||||
}),
|
||||
}));
|
||||
|
||||
let instructions = model.get_model_instructions(Some(Personality::Friendly));
|
||||
let instructions = model.get_model_instructions(Some(Personality::friendly()));
|
||||
|
||||
assert_eq!(instructions, "base");
|
||||
}
|
||||
@@ -666,15 +670,15 @@ mod tests {
|
||||
fn get_personality_message() {
|
||||
let personality_variables = personality_variables();
|
||||
assert_eq!(
|
||||
personality_variables.get_personality_message(Some(Personality::Friendly)),
|
||||
personality_variables.get_personality_message(Some(Personality::friendly())),
|
||||
Some("friendly".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
personality_variables.get_personality_message(Some(Personality::Pragmatic)),
|
||||
personality_variables.get_personality_message(Some(Personality::pragmatic())),
|
||||
Some("pragmatic".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
personality_variables.get_personality_message(Some(Personality::None)),
|
||||
personality_variables.get_personality_message(Some(Personality::none())),
|
||||
Some(String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -688,15 +692,15 @@ mod tests {
|
||||
personality_pragmatic: None,
|
||||
};
|
||||
assert_eq!(
|
||||
personality_variables.get_personality_message(Some(Personality::Friendly)),
|
||||
personality_variables.get_personality_message(Some(Personality::friendly())),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
personality_variables.get_personality_message(Some(Personality::Pragmatic)),
|
||||
personality_variables.get_personality_message(Some(Personality::pragmatic())),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
personality_variables.get_personality_message(Some(Personality::None)),
|
||||
personality_variables.get_personality_message(Some(Personality::none())),
|
||||
Some(String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -710,15 +714,15 @@ mod tests {
|
||||
personality_pragmatic: Some("pragmatic".to_string()),
|
||||
};
|
||||
assert_eq!(
|
||||
personality_variables.get_personality_message(Some(Personality::Friendly)),
|
||||
personality_variables.get_personality_message(Some(Personality::friendly())),
|
||||
Some("friendly".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
personality_variables.get_personality_message(Some(Personality::Pragmatic)),
|
||||
personality_variables.get_personality_message(Some(Personality::pragmatic())),
|
||||
Some("pragmatic".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
personality_variables.get_personality_message(Some(Personality::None)),
|
||||
personality_variables.get_personality_message(Some(Personality::none())),
|
||||
Some(String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
+59
-9
@@ -63,6 +63,8 @@ use codex_app_server_protocol::GetAccountRateLimitsResponse;
|
||||
use codex_app_server_protocol::ListMcpServerStatusParams;
|
||||
use codex_app_server_protocol::ListMcpServerStatusResponse;
|
||||
use codex_app_server_protocol::McpServerStatus;
|
||||
use codex_app_server_protocol::PersonalitiesListParams;
|
||||
use codex_app_server_protocol::PersonalitiesListResponse;
|
||||
use codex_app_server_protocol::PluginInstallParams;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginListParams;
|
||||
@@ -1902,6 +1904,17 @@ impl App {
|
||||
});
|
||||
}
|
||||
|
||||
fn fetch_personalities_list(&mut self, app_server: &AppServerSession, cwd: PathBuf) {
|
||||
let request_handle = app_server.request_handle();
|
||||
let app_event_tx = self.app_event_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = fetch_personalities_list(request_handle, cwd.clone())
|
||||
.await
|
||||
.map_err(|err| err.to_string());
|
||||
app_event_tx.send(AppEvent::PersonalitiesLoaded { cwd, result });
|
||||
});
|
||||
}
|
||||
|
||||
fn fetch_plugin_detail(
|
||||
&mut self,
|
||||
app_server: &AppServerSession,
|
||||
@@ -2261,7 +2274,7 @@ impl App {
|
||||
*summary,
|
||||
*service_tier,
|
||||
collaboration_mode.clone(),
|
||||
*personality,
|
||||
personality.clone(),
|
||||
final_output_json_schema.clone(),
|
||||
)
|
||||
.await?;
|
||||
@@ -4281,6 +4294,9 @@ impl App {
|
||||
AppEvent::FetchPluginsList { cwd } => {
|
||||
self.fetch_plugins_list(app_server, cwd);
|
||||
}
|
||||
AppEvent::FetchPersonalitiesList { cwd } => {
|
||||
self.fetch_personalities_list(app_server, cwd);
|
||||
}
|
||||
AppEvent::OpenPluginDetailLoading {
|
||||
plugin_display_name,
|
||||
} => {
|
||||
@@ -4302,6 +4318,22 @@ impl App {
|
||||
AppEvent::PluginsLoaded { cwd, result } => {
|
||||
self.chat_widget.on_plugins_loaded(cwd, result);
|
||||
}
|
||||
AppEvent::PersonalitiesLoaded { cwd, result } => match result {
|
||||
Ok(response) => {
|
||||
let personalities = response
|
||||
.data
|
||||
.into_iter()
|
||||
.find(|entry| entry.cwd == cwd)
|
||||
.map(|entry| entry.personalities)
|
||||
.unwrap_or_default();
|
||||
self.chat_widget
|
||||
.open_personality_popup_with_items(&personalities);
|
||||
}
|
||||
Err(err) => {
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to load personalities: {err}"));
|
||||
}
|
||||
},
|
||||
AppEvent::FetchPluginDetail { cwd, params } => {
|
||||
self.fetch_plugin_detail(app_server, cwd, params);
|
||||
}
|
||||
@@ -4864,12 +4896,12 @@ impl App {
|
||||
let profile = self.active_profile.as_deref();
|
||||
match ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(profile)
|
||||
.set_personality(Some(personality))
|
||||
.set_personality(Some(personality.clone()))
|
||||
.apply()
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
let label = Self::personality_label(personality);
|
||||
let label = Self::personality_label(&personality);
|
||||
let mut message = format!("Personality set to {label}");
|
||||
if let Some(profile) = profile {
|
||||
message.push_str(" for ");
|
||||
@@ -5682,7 +5714,7 @@ impl App {
|
||||
}
|
||||
|
||||
fn on_update_personality(&mut self, personality: Personality) {
|
||||
self.config.personality = Some(personality);
|
||||
self.config.personality = Some(personality.clone());
|
||||
self.chat_widget.set_personality(personality);
|
||||
}
|
||||
|
||||
@@ -5709,11 +5741,12 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
fn personality_label(personality: Personality) -> &'static str {
|
||||
match personality {
|
||||
Personality::None => "None",
|
||||
Personality::Friendly => "Friendly",
|
||||
Personality::Pragmatic => "Pragmatic",
|
||||
fn personality_label(personality: &Personality) -> String {
|
||||
match personality.as_str() {
|
||||
Personality::NONE => "None".to_string(),
|
||||
Personality::FRIENDLY => "Friendly".to_string(),
|
||||
Personality::PRAGMATIC => "Pragmatic".to_string(),
|
||||
_ => personality.as_str().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6018,6 +6051,23 @@ async fn fetch_plugins_list(
|
||||
.wrap_err("plugin/list failed in TUI")
|
||||
}
|
||||
|
||||
async fn fetch_personalities_list(
|
||||
request_handle: AppServerRequestHandle,
|
||||
cwd: PathBuf,
|
||||
) -> Result<PersonalitiesListResponse> {
|
||||
let cwd = AbsolutePathBuf::try_from(cwd).wrap_err("personality list cwd must be absolute")?;
|
||||
let request_id = RequestId::String(format!("personality-list-{}", Uuid::new_v4()));
|
||||
request_handle
|
||||
.request_typed(ClientRequest::PersonalitiesList {
|
||||
request_id,
|
||||
params: PersonalitiesListParams {
|
||||
cwds: Some(vec![cwd]),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("personalities/list failed in TUI")
|
||||
}
|
||||
|
||||
async fn fetch_plugin_detail(
|
||||
request_handle: AppServerRequestHandle,
|
||||
params: PluginReadParams,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_app_server_protocol::McpServerStatus;
|
||||
use codex_app_server_protocol::PersonalitiesListResponse;
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
@@ -183,12 +184,23 @@ pub(crate) enum AppEvent {
|
||||
cwd: PathBuf,
|
||||
},
|
||||
|
||||
/// Fetch available personalities for the provided working directory.
|
||||
FetchPersonalitiesList {
|
||||
cwd: PathBuf,
|
||||
},
|
||||
|
||||
/// Result of fetching plugin marketplace state.
|
||||
PluginsLoaded {
|
||||
cwd: PathBuf,
|
||||
result: Result<PluginListResponse, String>,
|
||||
},
|
||||
|
||||
/// Result of fetching available personalities.
|
||||
PersonalitiesLoaded {
|
||||
cwd: PathBuf,
|
||||
result: Result<PersonalitiesListResponse, String>,
|
||||
},
|
||||
|
||||
/// Replace the plugins popup with a plugin-detail loading state.
|
||||
OpenPluginDetailLoading {
|
||||
plugin_display_name: String,
|
||||
|
||||
@@ -80,6 +80,7 @@ use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::McpServerStartupState;
|
||||
use codex_app_server_protocol::McpServerStatusUpdatedNotification;
|
||||
use codex_app_server_protocol::PersonalityMetadata;
|
||||
use codex_app_server_protocol::ServerNotification;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
@@ -5847,8 +5848,9 @@ impl ChatWidget {
|
||||
let personality = self
|
||||
.config
|
||||
.personality
|
||||
.clone()
|
||||
.filter(|_| self.config.features.enabled(Feature::Personality))
|
||||
.filter(|_| self.current_model_supports_personality());
|
||||
.filter(|personality| !personality.is_none());
|
||||
let service_tier = self.config.service_tier.map(Some);
|
||||
let op = AppCommand::user_turn(
|
||||
items,
|
||||
@@ -7783,26 +7785,28 @@ impl ChatWidget {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if !self.current_model_supports_personality() {
|
||||
let current_model = self.current_model();
|
||||
self.add_error_message(format!(
|
||||
"Current model ({current_model}) doesn't support personalities. Try /model to pick a different model."
|
||||
));
|
||||
return;
|
||||
}
|
||||
self.open_personality_popup_for_current_model();
|
||||
self.app_event_tx.send(AppEvent::FetchPersonalitiesList {
|
||||
cwd: self.config.cwd.to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
fn open_personality_popup_for_current_model(&mut self) {
|
||||
let current_personality = self.config.personality.unwrap_or(Personality::Friendly);
|
||||
let personalities = [Personality::Friendly, Personality::Pragmatic];
|
||||
let supports_personality = self.current_model_supports_personality();
|
||||
pub(crate) fn open_personality_popup_with_items(
|
||||
&mut self,
|
||||
personalities: &[PersonalityMetadata],
|
||||
) {
|
||||
let current_personality = self
|
||||
.config
|
||||
.personality
|
||||
.clone()
|
||||
.unwrap_or_else(Personality::pragmatic);
|
||||
|
||||
let items: Vec<SelectionItem> = personalities
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|personality| {
|
||||
let name = Self::personality_label(personality).to_string();
|
||||
let description = Some(Self::personality_description(personality).to_string());
|
||||
let personality_id = Personality::from(personality.name.clone());
|
||||
let name = Self::personality_label(&personality_id);
|
||||
let description = Some(personality.description.clone());
|
||||
let selected_personality = personality_id.clone();
|
||||
let actions: Vec<SelectionAction> = vec![Box::new(move |tx| {
|
||||
tx.send(AppEvent::CodexOp(
|
||||
AppCommand::override_turn_context(
|
||||
@@ -7816,18 +7820,19 @@ impl ChatWidget {
|
||||
/*summary*/ None,
|
||||
/*service_tier*/ None,
|
||||
/*collaboration_mode*/ None,
|
||||
Some(personality),
|
||||
Some(selected_personality.clone()),
|
||||
)
|
||||
.into_core(),
|
||||
));
|
||||
tx.send(AppEvent::UpdatePersonality(personality));
|
||||
tx.send(AppEvent::PersistPersonalitySelection { personality });
|
||||
tx.send(AppEvent::UpdatePersonality(selected_personality.clone()));
|
||||
tx.send(AppEvent::PersistPersonalitySelection {
|
||||
personality: selected_personality.clone(),
|
||||
});
|
||||
})];
|
||||
SelectionItem {
|
||||
name,
|
||||
description,
|
||||
is_current: current_personality == personality,
|
||||
is_disabled: !supports_personality,
|
||||
is_current: current_personality == personality_id,
|
||||
actions,
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
@@ -9659,20 +9664,6 @@ impl ChatWidget {
|
||||
.set_plugins_command_enabled(self.config.features.enabled(Feature::Plugins));
|
||||
}
|
||||
|
||||
fn current_model_supports_personality(&self) -> bool {
|
||||
let model = self.current_model();
|
||||
self.model_catalog
|
||||
.try_list_models()
|
||||
.ok()
|
||||
.and_then(|models| {
|
||||
models
|
||||
.into_iter()
|
||||
.find(|preset| preset.model == model)
|
||||
.map(|preset| preset.supports_personality)
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Return whether the effective model currently advertises image-input support.
|
||||
///
|
||||
/// We intentionally default to `true` when model metadata cannot be read so transient catalog
|
||||
@@ -9821,19 +9812,12 @@ impl ChatWidget {
|
||||
self.bottom_pane.set_collaboration_mode_indicator(indicator);
|
||||
}
|
||||
|
||||
fn personality_label(personality: Personality) -> &'static str {
|
||||
match personality {
|
||||
Personality::None => "None",
|
||||
Personality::Friendly => "Friendly",
|
||||
Personality::Pragmatic => "Pragmatic",
|
||||
}
|
||||
}
|
||||
|
||||
fn personality_description(personality: Personality) -> &'static str {
|
||||
match personality {
|
||||
Personality::None => "No personality instructions.",
|
||||
Personality::Friendly => "Warm, collaborative, and helpful.",
|
||||
Personality::Pragmatic => "Concise, task-focused, and direct.",
|
||||
fn personality_label(personality: &Personality) -> String {
|
||||
match personality.as_str() {
|
||||
"none" => "None".to_string(),
|
||||
"friendly" => "Friendly".to_string(),
|
||||
"pragmatic" => "Pragmatic".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user