@bermudi/pi-delegate 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/settings.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
5
+ import { VALID_THINKING } from "./constants.ts";
6
+
7
+ export interface AgentOverride {
8
+ model?: string;
9
+ thinking?: ThinkingLevel;
10
+ tools?: string[];
11
+ }
4
12
 
5
13
  export interface DelegateSettings {
6
- agentOverrides?: Record<
7
- string,
8
- { model?: string; thinking?: string; tools?: string[]; skills?: string[] }
9
- >;
14
+ agentOverrides?: Record<string, AgentOverride>;
15
+ agentOverridesByParentModel?: Record<string, Record<string, AgentOverride>>;
10
16
  }
11
17
 
12
18
  /** Read and validate a JSON settings object, returning null on I/O or parse errors. */
@@ -16,29 +22,192 @@ export function readDelegateSettingsFile(
16
22
  try {
17
23
  const raw = fs.readFileSync(filePath, "utf-8");
18
24
  const parsed = JSON.parse(raw);
19
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
25
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
26
+ console.warn(
27
+ `[delegate] ignoring malformed settings file ${filePath}: expected a JSON object.`,
28
+ );
20
29
  return null;
30
+ }
21
31
  return parsed as Record<string, unknown>;
22
- } catch {
32
+ } catch (error) {
33
+ if (
34
+ error instanceof Error &&
35
+ "code" in error &&
36
+ (error as NodeJS.ErrnoException).code === "ENOENT"
37
+ ) {
38
+ return null;
39
+ }
40
+ console.warn(
41
+ `[delegate] could not read settings file ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
42
+ );
23
43
  return null;
24
44
  }
25
45
  }
26
46
 
47
+ function isRecord(value: unknown): value is Record<string, unknown> {
48
+ return value !== null && typeof value === "object" && !Array.isArray(value);
49
+ }
50
+
51
+ function normalizeOverride(
52
+ raw: unknown,
53
+ source: string,
54
+ agentName: string,
55
+ ): AgentOverride | null {
56
+ if (!isRecord(raw)) {
57
+ console.warn(
58
+ `[delegate] ignoring malformed settings override for agent '${agentName}' in ${source}: expected an object.`,
59
+ );
60
+ return null;
61
+ }
62
+
63
+ const result: AgentOverride = {};
64
+ for (const [key, value] of Object.entries(raw)) {
65
+ if (key === "model") {
66
+ if (typeof value !== "string" || value.trim().length === 0) {
67
+ console.warn(
68
+ `[delegate] ignoring malformed settings override for agent '${agentName}' in ${source}: model must be a nonempty string.`,
69
+ );
70
+ return null;
71
+ }
72
+ result.model = value.trim();
73
+ } else if (key === "thinking") {
74
+ if (typeof value !== "string" || !VALID_THINKING.has(value)) {
75
+ console.warn(
76
+ `[delegate] ignoring malformed settings override for agent '${agentName}' in ${source}: thinking must be a supported level.`,
77
+ );
78
+ return null;
79
+ }
80
+ result.thinking = value as ThinkingLevel;
81
+ } else if (key === "tools") {
82
+ if (
83
+ !Array.isArray(value) ||
84
+ value.some((tool) => typeof tool !== "string")
85
+ ) {
86
+ console.warn(
87
+ `[delegate] ignoring malformed settings override for agent '${agentName}' in ${source}: tools must be a string array.`,
88
+ );
89
+ return null;
90
+ }
91
+ result.tools = [...value];
92
+ } else if (key === "skills") {
93
+ console.warn(
94
+ `[delegate] ignoring unsupported skills override for agent '${agentName}' in ${source}: per-agent skill filtering is not supported.`,
95
+ );
96
+ return null;
97
+ } else {
98
+ console.warn(
99
+ `[delegate] ignoring malformed settings override for agent '${agentName}' in ${source}: unknown field '${key}'.`,
100
+ );
101
+ return null;
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+
107
+ function normalizeOverrides(
108
+ raw: unknown,
109
+ source: string,
110
+ ): Record<string, AgentOverride> {
111
+ if (!isRecord(raw)) {
112
+ console.warn(
113
+ `[delegate] ignoring malformed agentOverrides in ${source}: expected an object.`,
114
+ );
115
+ return {};
116
+ }
117
+
118
+ const result: Record<string, AgentOverride> = {};
119
+ const seenNames = new Map<string, string>();
120
+ for (const [agentName, value] of Object.entries(raw)) {
121
+ const normalizedAgentName = agentName.trim();
122
+ if (normalizedAgentName.length === 0) {
123
+ console.warn(
124
+ `[delegate] ignoring malformed settings override in ${source}: agent name must be nonempty.`,
125
+ );
126
+ continue;
127
+ }
128
+ const previousName = seenNames.get(normalizedAgentName);
129
+ if (previousName !== undefined) {
130
+ console.warn(
131
+ `[delegate] ignoring duplicate settings override in ${source}: agent keys '${previousName}' and '${agentName}' both normalize to '${normalizedAgentName}'.`,
132
+ );
133
+ continue;
134
+ }
135
+ seenNames.set(normalizedAgentName, agentName);
136
+ const override = normalizeOverride(value, source, normalizedAgentName);
137
+ if (override) result[normalizedAgentName] = override;
138
+ }
139
+ return result;
140
+ }
141
+
142
+ function normalizeOverridesByParentModel(
143
+ raw: unknown,
144
+ source: string,
145
+ ): Record<string, Record<string, AgentOverride>> {
146
+ if (!isRecord(raw)) {
147
+ console.warn(
148
+ `[delegate] ignoring malformed agentOverridesByParentModel in ${source}: expected an object.`,
149
+ );
150
+ return {};
151
+ }
152
+
153
+ const result: Record<string, Record<string, AgentOverride>> = {};
154
+ const seenModels = new Map<string, string>();
155
+ for (const [parentModel, overrides] of Object.entries(raw)) {
156
+ const normalizedParentModel = parentModel.trim();
157
+ if (normalizedParentModel.length === 0) {
158
+ console.warn(
159
+ `[delegate] ignoring malformed parent-model override in ${source}: model key must be nonempty.`,
160
+ );
161
+ continue;
162
+ }
163
+ const previousModel = seenModels.get(normalizedParentModel);
164
+ if (previousModel !== undefined) {
165
+ console.warn(
166
+ `[delegate] ignoring duplicate parent-model override in ${source}: model keys '${previousModel}' and '${parentModel}' both normalize to '${normalizedParentModel}'.`,
167
+ );
168
+ continue;
169
+ }
170
+ seenModels.set(normalizedParentModel, parentModel);
171
+ result[normalizedParentModel] = normalizeOverrides(
172
+ overrides,
173
+ `${source} (parent model '${normalizedParentModel}')`,
174
+ );
175
+ }
176
+ return result;
177
+ }
178
+
27
179
  function getDelegateSettings(filePath: string): DelegateSettings | null {
28
180
  const settings = readDelegateSettingsFile(filePath);
29
181
  if (
30
182
  !settings?.delegate ||
31
183
  typeof settings.delegate !== "object" ||
32
184
  Array.isArray(settings.delegate)
33
- )
185
+ ) {
186
+ if (settings?.delegate !== undefined) {
187
+ console.warn(
188
+ `[delegate] ignoring malformed delegate settings in ${filePath}: expected an object.`,
189
+ );
190
+ }
34
191
  return null;
35
- return settings.delegate as DelegateSettings;
192
+ }
193
+ const raw = settings.delegate as Record<string, unknown>;
194
+ const result: DelegateSettings = {};
195
+ if (raw.agentOverrides !== undefined) {
196
+ result.agentOverrides = normalizeOverrides(raw.agentOverrides, filePath);
197
+ }
198
+ if (raw.agentOverridesByParentModel !== undefined) {
199
+ result.agentOverridesByParentModel = normalizeOverridesByParentModel(
200
+ raw.agentOverridesByParentModel,
201
+ filePath,
202
+ );
203
+ }
204
+ return result;
36
205
  }
37
206
 
38
207
  const delegateSettingsCache = new Map<string, DelegateSettings | null>();
39
208
 
40
209
  /** Load merged delegate settings: project overrides user.
41
- * Result is cached per cwd for the lifetime of the delegate call. */
210
+ * Result is cached per cwd until the next delegate dispatch clears it. */
42
211
  export function loadDelegateSettings(cwd: string): DelegateSettings | null {
43
212
  const key = path.resolve(cwd);
44
213
  const cached = delegateSettingsCache.get(key);
@@ -68,11 +237,64 @@ export function loadDelegateSettings(cwd: string): DelegateSettings | null {
68
237
  return null;
69
238
  }
70
239
  const result: DelegateSettings = {
71
- agentOverrides: {
72
- ...(user?.agentOverrides ?? {}),
73
- ...(project?.agentOverrides ?? {}),
74
- },
240
+ ...(user?.agentOverrides || project?.agentOverrides
241
+ ? {
242
+ agentOverrides: mergeOverrides(
243
+ user?.agentOverrides,
244
+ project?.agentOverrides,
245
+ ),
246
+ }
247
+ : {}),
248
+ ...(user?.agentOverridesByParentModel ||
249
+ project?.agentOverridesByParentModel
250
+ ? {
251
+ agentOverridesByParentModel: mergeParentModelOverrides(
252
+ user?.agentOverridesByParentModel,
253
+ project?.agentOverridesByParentModel,
254
+ ),
255
+ }
256
+ : {}),
75
257
  };
76
258
  delegateSettingsCache.set(key, result);
77
259
  return result;
78
260
  }
261
+
262
+ function mergeOverride(
263
+ base: AgentOverride | undefined,
264
+ override: AgentOverride | undefined,
265
+ ): AgentOverride {
266
+ return { ...(base ?? {}), ...(override ?? {}) };
267
+ }
268
+
269
+ function mergeOverrides(
270
+ user: Record<string, AgentOverride> | undefined,
271
+ project: Record<string, AgentOverride> | undefined,
272
+ ): Record<string, AgentOverride> {
273
+ const result: Record<string, AgentOverride> = {};
274
+ for (const name of new Set([
275
+ ...Object.keys(user ?? {}),
276
+ ...Object.keys(project ?? {}),
277
+ ])) {
278
+ result[name] = mergeOverride(user?.[name], project?.[name]);
279
+ }
280
+ return result;
281
+ }
282
+
283
+ function mergeParentModelOverrides(
284
+ user: Record<string, Record<string, AgentOverride>> | undefined,
285
+ project: Record<string, Record<string, AgentOverride>> | undefined,
286
+ ): Record<string, Record<string, AgentOverride>> {
287
+ const result: Record<string, Record<string, AgentOverride>> = {};
288
+ for (const model of new Set([
289
+ ...Object.keys(user ?? {}),
290
+ ...Object.keys(project ?? {}),
291
+ ])) {
292
+ result[model] = mergeOverrides(user?.[model], project?.[model]);
293
+ }
294
+ return result;
295
+ }
296
+
297
+ /** Clear settings read from earlier delegate calls so edits are visible. */
298
+ export function clearDelegateSettingsCache(): void {
299
+ delegateSettingsCache.clear();
300
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
3
3
  import {
4
+ BUILTIN_AGENT_NAMES,
4
5
  DEFAULT_AGENT_NAME,
5
6
  DEFAULT_TOOLS,
6
7
  VALID_THINKING,
@@ -8,7 +9,7 @@ import {
8
9
  import { TOOL_FACTORIES, resolveToolGroups } from "./tools.ts";
9
10
  import { configFor } from "./pool.ts";
10
11
  import { isSessionBusy } from "./tickets.ts";
11
- import { buildSubagentSystemPrompt } from "./agents.ts";
12
+ import { BUILTIN_AGENT_CONFIGS, buildSubagentSystemPrompt } from "./agents.ts";
12
13
  import { buildParentTranscript } from "./parent-context.ts";
13
14
  import { findAvailableAlternative, resolveModelRequest } from "./model.ts";
14
15
  import { resolveModelSpec } from "./config.ts";
@@ -91,6 +92,52 @@ export function validateTasks(
91
92
  agents: Map<string, AgentConfig>,
92
93
  parentModelId: string | undefined,
93
94
  ): DelegateToolResult | null {
95
+ const unknown: string[] = [];
96
+ for (const task of tasks) {
97
+ if (
98
+ task.agent &&
99
+ !(BUILTIN_AGENT_NAMES as readonly string[]).includes(task.agent) &&
100
+ !agents.has(task.agent)
101
+ ) {
102
+ unknown.push(task.agent);
103
+ }
104
+ }
105
+ if (unknown.length) {
106
+ const names = [...new Set([...BUILTIN_AGENT_NAMES, ...agents.keys()])];
107
+ return noticeResult(
108
+ `Unknown agent(s): ${unknown.join(", ")}. Available: ${names.join(", ") || "(none)"}. Call delegate with an empty tasks array for help.`,
109
+ tasks,
110
+ parentModelId,
111
+ );
112
+ }
113
+
114
+ // Scratch sessions are deliberately one-shot. This check uses the
115
+ // effective workspace, so reviewer gets the same protection even when the
116
+ // caller omits workspace. Explicit scratch is never silently promoted to
117
+ // shared.
118
+ for (const [index, task] of tasks.entries()) {
119
+ const agent = task.agent
120
+ ? (agents.get(task.agent) ?? BUILTIN_AGENT_CONFIGS[task.agent])
121
+ : undefined;
122
+ const workspace = task.workspace ?? agent?.workspace ?? "shared";
123
+ const sessionAction = task.sessionAction ?? task.action;
124
+ if (
125
+ workspace === "scratch" &&
126
+ (task.sessionId || task.resumeFrom || sessionAction !== undefined)
127
+ ) {
128
+ const defaultText =
129
+ task.workspace === undefined && agent?.workspace === "scratch"
130
+ ? "defaults to workspace `scratch`"
131
+ : "uses workspace `scratch`";
132
+ const persistentAgent = task.agent ?? "agent";
133
+ return noticeResult(
134
+ `${formatTaskRef(index, task.id)}: Agent \`${persistentAgent}\` ${defaultText}, which is one-shot and cannot use \`sessionId\`, \`resumeFrom\`, or session actions. Set \`workspace: "shared"\` to use a persistent ${persistentAgent}.`,
135
+ tasks,
136
+ parentModelId,
137
+ );
138
+ }
139
+ }
140
+
94
141
  // Disallow same sessionId across multiple parallel tasks (one agent can't serve two prompts concurrently).
95
142
  const sessionIds = tasks.map((t) => t.sessionId).filter(Boolean) as string[];
96
143
  const duplicateSessions = sessionIds.filter(
@@ -136,21 +183,6 @@ export function validateTasks(
136
183
  return noticeResult(duplicateIds.join(" "), tasks, parentModelId);
137
184
  }
138
185
 
139
- const unknown: string[] = [];
140
- for (const t of tasks) {
141
- if (t.agent && t.agent !== DEFAULT_AGENT_NAME && !agents.has(t.agent)) {
142
- unknown.push(t.agent);
143
- }
144
- }
145
- if (unknown.length) {
146
- const names = [DEFAULT_AGENT_NAME, ...agents.keys()];
147
- return noticeResult(
148
- `Unknown agent(s): ${unknown.join(", ")}. Available: ${names.join(", ") || "(none)"}. Call delegate with an empty tasks array for help.`,
149
- tasks,
150
- parentModelId,
151
- );
152
- }
153
-
154
186
  return null;
155
187
  }
156
188
 
@@ -187,11 +219,21 @@ export function resolveTasks(
187
219
 
188
220
  return tasks.map((t, i) => {
189
221
  const isDefaultAgent = t.agent === DEFAULT_AGENT_NAME;
190
- const agent = t.agent && !isDefaultAgent ? agents.get(t.agent) : undefined;
222
+ const agent = t.agent
223
+ ? (agents.get(t.agent) ?? BUILTIN_AGENT_CONFIGS[t.agent])
224
+ : undefined;
225
+ const isBuiltinAgent = agent?.builtin === true;
191
226
  const cwd = resolveCwd(t.cwd ?? ctx.cwd, ctx.cwd);
192
227
 
193
228
  // Load settings-based overrides for this agent
194
229
  const settings = loadDelegateSettings(cwd);
230
+ const parentModelKey = ctx.model
231
+ ? `${ctx.model.provider}/${ctx.model.id}`
232
+ : undefined;
233
+ const parentModelOverride =
234
+ t.agent && !isDefaultAgent && parentModelKey
235
+ ? settings?.agentOverridesByParentModel?.[parentModelKey]?.[t.agent]
236
+ : undefined;
195
237
  const agentOverride =
196
238
  t.agent && !isDefaultAgent && settings?.agentOverrides?.[t.agent]
197
239
  ? settings.agentOverrides[t.agent]
@@ -208,7 +250,8 @@ export function resolveTasks(
208
250
  );
209
251
  let tools: string[] = [];
210
252
  const warnings: string[] = [];
211
- if (t.workspace === "scratch") {
253
+ const workspace = t.workspace ?? agent?.workspace ?? "shared";
254
+ if (workspace === "scratch") {
212
255
  warnings.push(
213
256
  "Scratch workspace: relative file changes run in a disposable CoW copy and are discarded.",
214
257
  );
@@ -233,9 +276,11 @@ export function resolveTasks(
233
276
  if (t.sessionAction !== "close" && t.sessionAction !== "list") {
234
277
  tools = resolveToolGroups(
235
278
  t.tools ??
279
+ parentModelOverride?.tools ??
236
280
  agentOverride?.tools ??
237
- agent?.tools ??
238
281
  (isDefaultAgent ? parentNativeTools : undefined) ??
282
+ (isBuiltinAgent ? agent?.tools : undefined) ??
283
+ agent?.tools ??
239
284
  (isPoolHit ? pooledConfig?.tools : undefined) ??
240
285
  DEFAULT_TOOLS,
241
286
  );
@@ -318,20 +363,32 @@ export function resolveTasks(
318
363
  let thinking: ThinkingLevel = "off";
319
364
 
320
365
  if (t.sessionAction !== "close" && t.sessionAction !== "list") {
366
+ const agentType = t.agent ?? "inline";
367
+ // The built-in `default` profile bypasses delegate/settings model
368
+ // overrides for backwards compatibility. The other built-ins accept
369
+ // task and settings.json model overrides, but deliberately ignore the
370
+ // legacy delegate.json agent model map so they inherit the parent unless
371
+ // an explicit modern override wins.
372
+ const modelSpec = isDefaultAgent
373
+ ? t.model
374
+ : isBuiltinAgent
375
+ ? (t.model ?? parentModelOverride?.model ?? agentOverride?.model)
376
+ : resolveModelSpec({
377
+ taskModel:
378
+ t.model ?? parentModelOverride?.model ?? agentOverride?.model,
379
+ agentType,
380
+ frontmatterModel: agent?.model,
381
+ });
382
+
321
383
  // A pool hit always runs its frozen model, but an explicitly requested
322
384
  // task/profile model still has to be resolved so checkout can reject a
323
385
  // contradictory request rather than silently discarding it. Naming the
324
386
  // built-in `default` profile is also explicit: it requests the live
325
387
  // parent model, so reuse fails clearly if the pool was frozen differently.
326
388
  if (pooledConfig) {
327
- const requestedModelSpec =
328
- t.model ??
329
- (t.agent && !isDefaultAgent
330
- ? (agentOverride?.model ?? agent?.model)
331
- : undefined);
332
- if (requestedModelSpec) {
389
+ if (modelSpec) {
333
390
  const requested = resolveModelRequest(
334
- requestedModelSpec,
391
+ modelSpec,
335
392
  ctx.modelRegistry,
336
393
  ctx.model,
337
394
  );
@@ -339,7 +396,7 @@ export function resolveTasks(
339
396
  modelSuffix = requested.strippedSuffix;
340
397
  if (!requestedModel) {
341
398
  throw new Error(
342
- `${formatTaskRef(i, t.id)}: requested model '${requestedModelSpec}' is not available. Check provider config or remove the model field to continue the pooled session.`,
399
+ `${formatTaskRef(i, t.id)}: requested model '${modelSpec}' is not available. Check provider config or remove the model field to continue the pooled session.`,
343
400
  );
344
401
  }
345
402
  } else if (isDefaultAgent) {
@@ -347,17 +404,6 @@ export function resolveTasks(
347
404
  }
348
405
  model = pooledConfig.model;
349
406
  } else {
350
- // The built-in `default` profile bypasses delegate.json and settings:
351
- // absent a task override, it means this exact live parent Model object.
352
- // Other tasks retain the normal task > config > frontmatter chain.
353
- const agentType = t.agent ?? "inline";
354
- const modelSpec = isDefaultAgent
355
- ? t.model
356
- : resolveModelSpec({
357
- taskModel: t.model ?? agentOverride?.model,
358
- agentType,
359
- frontmatterModel: agent?.model,
360
- });
361
407
  const resolvedRequest = modelSpec
362
408
  ? resolveModelRequest(modelSpec, ctx.modelRegistry, ctx.model)
363
409
  : undefined;
@@ -376,7 +422,7 @@ export function resolveTasks(
376
422
  );
377
423
  }
378
424
 
379
- model = isDefaultAgent
425
+ model = isBuiltinAgent
380
426
  ? (resolvedModel ?? ctx.model)
381
427
  : (resolvedModel ??
382
428
  findAvailableAlternative(ctx.model, ctx.modelRegistry) ??
@@ -398,15 +444,28 @@ export function resolveTasks(
398
444
  // changed, rather than silently reusing a stale frozen value. The final
399
445
  // pooled fallback is reachable only when parentDefaults.thinking is
400
446
  // undefined (headless parent without a thinking level).
401
- const thinkingRaw =
402
- t.thinking ??
403
- agentOverride?.thinking ??
404
- agent?.thinking ??
405
- (isPoolHit && !isDefaultAgent ? pooledConfig?.thinking : undefined) ??
406
- modelSuffix ??
407
- (isDefaultAgent ? parentDefaults.thinking : undefined) ??
408
- (isPoolHit ? pooledConfig?.thinking : undefined) ??
409
- "off";
447
+ const thinkingRaw = isBuiltinAgent
448
+ ? isDefaultAgent
449
+ ? (t.thinking ??
450
+ parentModelOverride?.thinking ??
451
+ agentOverride?.thinking ??
452
+ modelSuffix ??
453
+ parentDefaults.thinking ??
454
+ (isPoolHit ? pooledConfig?.thinking : undefined) ??
455
+ "off")
456
+ : (t.thinking ??
457
+ parentModelOverride?.thinking ??
458
+ agentOverride?.thinking ??
459
+ (isPoolHit ? pooledConfig?.thinking : undefined) ??
460
+ modelSuffix ??
461
+ parentDefaults.thinking ??
462
+ "off")
463
+ : (t.thinking ??
464
+ agentOverride?.thinking ??
465
+ agent?.thinking ??
466
+ (isPoolHit ? pooledConfig?.thinking : undefined) ??
467
+ modelSuffix ??
468
+ "off");
410
469
  thinking = VALID_THINKING.has(thinkingRaw)
411
470
  ? (thinkingRaw as ThinkingLevel)
412
471
  : "off";
@@ -423,7 +482,7 @@ export function resolveTasks(
423
482
  ...t,
424
483
  id: t.id,
425
484
  cwd,
426
- workspace: t.workspace ?? "shared",
485
+ workspace,
427
486
  systemPrompt,
428
487
  model: model!,
429
488
  tools,
@@ -433,9 +492,7 @@ export function resolveTasks(
433
492
  prompt: prompt ?? "",
434
493
  // Keep the built-in selector visible in progress/results. Omitted-agent
435
494
  // inline tasks retain the established `ad-hoc` label and config namespace.
436
- agentName: isDefaultAgent
437
- ? DEFAULT_AGENT_NAME
438
- : (agent?.name ?? "ad-hoc"),
495
+ agentName: agent?.name ?? "ad-hoc",
439
496
  warnings,
440
497
  reuseIntent: {
441
498
  model: requestedModel,
package/types.ts CHANGED
@@ -18,9 +18,15 @@ export interface AgentConfig {
18
18
  name: string;
19
19
  description: string;
20
20
  model?: string;
21
- thinking: ThinkingLevel;
21
+ /** Markdown agents default invalid/omitted values to "off". Built-ins omit
22
+ * this field so they can inherit the parent's thinking level. */
23
+ thinking?: ThinkingLevel;
22
24
  tools: string[];
23
25
  systemPrompt: string;
26
+ /** Built-in profiles are immutable and cannot be shadowed by Markdown. */
27
+ builtin?: boolean;
28
+ /** Default workspace for a built-in profile. Custom agents use shared. */
29
+ workspace?: WorkspaceMode;
24
30
  /** Origin of the profile. `claude` denotes imported .claude/agents files. */
25
31
  scope?: "project" | "global" | "claude";
26
32
  }