Skip to content

Commit e805dbc

Browse files
committed
Restore session list overflow toggle
1 parent 0ac2d3b commit e805dbc

21 files changed

Lines changed: 767 additions & 95 deletions

openapi.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,11 @@ components:
620620
name: { type: string, minLength: 1 }
621621
backend: { type: string, minLength: 1 }
622622
model: { type: string, minLength: 1 }
623+
reasoningEffort:
624+
type: string
625+
nullable: true
626+
allOf:
627+
- $ref: '#/components/schemas/SettingsReasoningEffort'
623628
createdAt: { type: string, minLength: 1 }
624629
AgentUpdateRequest:
625630
allOf:
@@ -630,9 +635,41 @@ components:
630635
additionalProperties: false
631636
properties:
632637
name: { type: string, minLength: 1 }
638+
description: { type: string }
639+
logo: { type: string }
640+
runtime: { type: string, minLength: 1 }
633641
backend: { type: string, minLength: 1 }
634642
model: { type: string, minLength: 1 }
643+
reasoningEffort:
644+
type: string
645+
nullable: true
646+
allOf:
647+
- $ref: '#/components/schemas/SettingsReasoningEffort'
648+
status:
649+
type: string
650+
enum: [offline, online]
651+
concurrency:
652+
type: integer
653+
minimum: 1
654+
owner: { type: string, minLength: 1 }
635655
createdAt: { type: string, minLength: 1 }
656+
updatedAt: { type: string, minLength: 1 }
657+
skills:
658+
type: array
659+
items:
660+
type: string
661+
minLength: 1
662+
recentWork:
663+
type: array
664+
items:
665+
type: string
666+
minLength: 1
667+
activity:
668+
type: array
669+
items:
670+
type: string
671+
minLength: 1
672+
instructions: { type: string }
636673
SkillCreateRequest:
637674
type: object
638675
additionalProperties: false
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { CODEX_BACKEND, CODEX_DEFAULT_STAGE_MODELS } from "adapters/codex";
2+
import { DEFAULT_REASONING_EFFORTS } from "devos/features/onboard/constants";
3+
import {
4+
loadInstanceConfig,
5+
saveInstanceConfig,
6+
} from "devos/features/onboard/instance-config";
7+
import type { OnboardInstanceConfig } from "devos/features/onboard/types/instance-config.types";
8+
import type { AgentUpdatePayload } from "../routes/types/entity-crud.types";
9+
import {
10+
applySettingsModelStageUpdate,
11+
findSettingsModelStage,
12+
pruneEmptyCodexSettings,
13+
} from "../settings/settings-models-config";
14+
import type {
15+
SettingsModelConfigKey,
16+
SettingsModelStageId,
17+
SettingsModelStageUpdate,
18+
} from "../settings/types/settings-models.types";
19+
import type {
20+
AgentReasoningEffort,
21+
AgentRecord,
22+
} from "../types/repositories.types";
23+
import type { DefaultAgentDefinition } from "./types/default-agents.types";
24+
25+
const DEFAULT_AGENT_DETAILS: Record<
26+
SettingsModelStageId,
27+
Pick<DefaultAgentDefinition, "description" | "instructions">
28+
> = {
29+
brainstorm: {
30+
description: "Explores task context and shapes the first workflow plan.",
31+
instructions: "Clarify intent, constraints, and success criteria.",
32+
},
33+
plan: {
34+
description: "Turns scoped work into an implementation plan.",
35+
instructions: "Produce a concise plan with verification steps.",
36+
},
37+
implement: {
38+
description: "Applies code changes for approved workflow tasks.",
39+
instructions: "Make focused edits and keep package boundaries intact.",
40+
},
41+
testing: {
42+
description: "Reviews and validates the completed implementation.",
43+
instructions: "Run relevant checks and report evidence before completion.",
44+
},
45+
};
46+
47+
const CONFIG_AGENT_UPDATE_FIELDS = new Set(["model", "reasoningEffort"]);
48+
49+
export class DefaultAgentsError extends Error {
50+
constructor(
51+
readonly status: number,
52+
message: string,
53+
) {
54+
super(message);
55+
this.name = "DefaultAgentsError";
56+
}
57+
}
58+
59+
export async function listDefaultAgents(
60+
workspacePath: string,
61+
): Promise<AgentRecord[]> {
62+
const config = await readInstanceConfig(workspacePath);
63+
return defaultAgentDefinitions().map((definition) =>
64+
renderDefaultAgent(definition, config),
65+
);
66+
}
67+
68+
export async function getDefaultAgent(
69+
workspacePath: string,
70+
id: string,
71+
): Promise<AgentRecord | null> {
72+
const definition = findDefaultAgentDefinition(id);
73+
if (!definition) return null;
74+
return renderDefaultAgent(
75+
definition,
76+
await readInstanceConfig(workspacePath),
77+
);
78+
}
79+
80+
export async function updateDefaultAgent(
81+
workspacePath: string,
82+
id: string,
83+
input: AgentUpdatePayload,
84+
): Promise<AgentRecord | null> {
85+
const definition = findDefaultAgentDefinition(id);
86+
if (!definition) return null;
87+
assertConfigAgentUpdate(input);
88+
const update = toSettingsUpdate(definition.id, input);
89+
const config = await readInstanceConfig(workspacePath);
90+
applySettingsModelStageUpdate(config, update, definition.configKey);
91+
pruneEmptyCodexSettings(config);
92+
await saveInstanceConfig(config);
93+
return renderDefaultAgent(definition, config);
94+
}
95+
96+
async function readInstanceConfig(
97+
workspacePath: string,
98+
): Promise<OnboardInstanceConfig> {
99+
const result = await loadInstanceConfig(workspacePath);
100+
if (!result.ok) {
101+
throw new DefaultAgentsError(404, result.message);
102+
}
103+
return result.config;
104+
}
105+
106+
function defaultAgentDefinitions(): DefaultAgentDefinition[] {
107+
return ["brainstorm", "plan", "implement", "testing"].map((id) => {
108+
const stage = findSettingsModelStage(id);
109+
if (!stage) {
110+
throw new DefaultAgentsError(500, `Default agent '${id}' is not mapped`);
111+
}
112+
return {
113+
...stage,
114+
...DEFAULT_AGENT_DETAILS[stage.id],
115+
};
116+
});
117+
}
118+
119+
function findDefaultAgentDefinition(
120+
id: string,
121+
): DefaultAgentDefinition | undefined {
122+
return defaultAgentDefinitions().find((definition) => definition.id === id);
123+
}
124+
125+
function renderDefaultAgent(
126+
definition: DefaultAgentDefinition,
127+
config: OnboardInstanceConfig,
128+
): AgentRecord {
129+
const updatedAt = config.$meta.updatedAt;
130+
return {
131+
id: definition.id,
132+
name: definition.label,
133+
description: definition.description,
134+
logo: "",
135+
runtime: CODEX_BACKEND,
136+
backend: CODEX_BACKEND,
137+
model: modelForStage(definition.configKey, config),
138+
reasoningEffort: reasoningForStage(definition.configKey, config),
139+
status: "online",
140+
concurrency: 1,
141+
owner: config.workspace.id,
142+
createdAt: updatedAt,
143+
updatedAt,
144+
skills: [],
145+
recentWork: [],
146+
activity: [],
147+
instructions: definition.instructions,
148+
};
149+
}
150+
151+
function modelForStage(
152+
configKey: SettingsModelConfigKey,
153+
config: OnboardInstanceConfig,
154+
): string {
155+
return (
156+
config.codex?.models?.[configKey] ??
157+
CODEX_DEFAULT_STAGE_MODELS[configKey] ??
158+
"gpt-5.5"
159+
);
160+
}
161+
162+
function reasoningForStage(
163+
configKey: SettingsModelConfigKey,
164+
config: OnboardInstanceConfig,
165+
): AgentReasoningEffort {
166+
return (
167+
config.codex?.reasoningEfforts?.[configKey] ??
168+
DEFAULT_REASONING_EFFORTS[configKey]
169+
);
170+
}
171+
172+
function assertConfigAgentUpdate(input: AgentUpdatePayload): void {
173+
const unsupportedFields = Object.keys(input).filter(
174+
(field) => !CONFIG_AGENT_UPDATE_FIELDS.has(field),
175+
);
176+
if (unsupportedFields.length > 0) {
177+
throw new DefaultAgentsError(
178+
400,
179+
"Config-backed agents only support model and reasoningEffort updates",
180+
);
181+
}
182+
}
183+
184+
function toSettingsUpdate(
185+
id: SettingsModelStageId,
186+
input: AgentUpdatePayload,
187+
): SettingsModelStageUpdate {
188+
return {
189+
id,
190+
...(input.model !== undefined ? { model: input.model } : {}),
191+
...(input.reasoningEffort !== undefined
192+
? { reasoningEffort: input.reasoningEffort }
193+
: {}),
194+
};
195+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import type {
2+
SettingsModelConfigKey,
3+
SettingsModelStageId,
4+
} from "../../settings/types/settings-models.types";
5+
6+
export interface DefaultAgentDefinition {
7+
id: SettingsModelStageId;
8+
label: string;
9+
configKey: SettingsModelConfigKey;
10+
description: string;
11+
instructions: string;
12+
}

packages/server/src/http/app-routes.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export function createAppRoutes(deps: AppDeps): RouteRegistryEntry[] {
9393
handleSettingsRoute(request, pathname, workspacePath),
9494
),
9595
route("entity-crud", (request, { pathname }) =>
96-
handleEntityCrudRoute(request, deps, pathname),
96+
handleEntityCrudRoute(request, deps, pathname, workspacePath),
9797
),
9898
route("notifications", (request, { pathname }) =>
9999
handleNotificationRoute(request, deps, pathname),
@@ -135,6 +135,7 @@ async function handleEntityCrudRoute(
135135
request: Request,
136136
deps: AppDeps,
137137
pathname: string,
138+
workspacePath: string,
138139
): Promise<Response | null> {
139140
const crudRoute = matchCrudRoute(pathname);
140141
if (!crudRoute) {
@@ -145,7 +146,7 @@ async function handleEntityCrudRoute(
145146
}
146147
const result = await handleEntityCrudRequest(
147148
request,
148-
{ db: deps.db },
149+
{ db: deps.db, workspacePath },
149150
crudRoute,
150151
);
151152
return result.body === undefined

packages/server/src/routes/entity-crud.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import type { ServerDatabase } from "devos-db";
2+
import {
3+
DefaultAgentsError,
4+
getDefaultAgent,
5+
listDefaultAgents,
6+
updateDefaultAgent,
7+
} from "../agents/default-agents";
28
import {
39
validateAgentCreatePayload,
410
validateAgentUpdatePayload,
@@ -34,6 +40,7 @@ const SKILL_UPDATE_FIELDS = [
3440

3541
interface EntityCrudDeps {
3642
db: ServerDatabase["db"];
43+
workspacePath: string;
3744
}
3845

3946
export function matchCrudRoute(pathname: string): CrudRouteMatch | null {
@@ -50,20 +57,32 @@ export async function handleEntityCrudRequest(
5057
route: CrudRouteMatch,
5158
): Promise<CrudResponseResult> {
5259
const service = createEntityCrudService(createEntityCrudRepository(deps.db));
53-
if (route.entity === "agents") {
54-
return handleAgentRequest(request, service, route.id);
60+
try {
61+
if (route.entity === "agents") {
62+
return handleAgentRequest(request, service, route.id, deps.workspacePath);
63+
}
64+
return handleSkillRequest(request, service, route.id);
65+
} catch (error) {
66+
if (error instanceof DefaultAgentsError) {
67+
return { status: error.status, body: { error: error.message } };
68+
}
69+
throw error;
5570
}
56-
return handleSkillRequest(request, service, route.id);
5771
}
5872

5973
async function handleAgentRequest(
6074
request: Request,
6175
service: ReturnType<typeof createEntityCrudService>,
6276
id: string | null,
77+
workspacePath: string,
6378
): Promise<CrudResponseResult> {
6479
if (id === null) {
6580
if (request.method === "GET") {
66-
return mapEntityResult(await service.listAgents());
81+
const result = await service.listAgents();
82+
if (result.status === "ok" && result.value.length === 0) {
83+
return { status: 200, body: await listDefaultAgents(workspacePath) };
84+
}
85+
return mapEntityResult(result);
6786
}
6887
if (request.method === "POST") {
6988
const parsed = await parseJsonBody(request);
@@ -80,7 +99,14 @@ async function handleAgentRequest(
8099
}
81100

82101
if (request.method === "GET") {
83-
return mapEntityResult(await service.getAgent(id));
102+
const result = await service.getAgent(id);
103+
if (result.status === "ok") {
104+
return mapEntityResult(result);
105+
}
106+
const defaultAgent = await getDefaultAgent(workspacePath, id);
107+
return defaultAgent
108+
? { status: 200, body: defaultAgent }
109+
: mapEntityResult(result);
84110
}
85111

86112
if (request.method === "PATCH") {
@@ -92,7 +118,18 @@ async function handleAgentRequest(
92118
if (!validated.ok) {
93119
return { status: 400, body: { error: validated.error } };
94120
}
95-
return mapEntityResult(await service.updateAgent(id, validated.value));
121+
const result = await service.updateAgent(id, validated.value);
122+
if (result.status === "ok") {
123+
return mapEntityResult(result);
124+
}
125+
const defaultAgent = await updateDefaultAgent(
126+
workspacePath,
127+
id,
128+
validated.value,
129+
);
130+
return defaultAgent
131+
? { status: 200, body: defaultAgent }
132+
: mapEntityResult(result);
96133
}
97134

98135
if (request.method === "DELETE") {

0 commit comments

Comments
 (0)