@markusylisiurunen/tau 0.2.63 → 0.2.65

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.
Files changed (62) hide show
  1. package/dist/core/async/cli.js +68 -104
  2. package/dist/core/async/cli.js.map +1 -1
  3. package/dist/core/async/http_protocol.js +119 -8
  4. package/dist/core/async/http_protocol.js.map +1 -1
  5. package/dist/core/async/http_server.js +120 -240
  6. package/dist/core/async/http_server.js.map +1 -1
  7. package/dist/core/async/index.js +1 -1
  8. package/dist/core/async/index.js.map +1 -1
  9. package/dist/core/async/server_config.js +161 -356
  10. package/dist/core/async/server_config.js.map +1 -1
  11. package/dist/core/async/session_manager.js +5 -17
  12. package/dist/core/async/session_manager.js.map +1 -1
  13. package/dist/core/async/telegram.js +268 -171
  14. package/dist/core/async/telegram.js.map +1 -1
  15. package/dist/core/auth/providers/openai_codex.js +4 -9
  16. package/dist/core/auth/providers/openai_codex.js.map +1 -1
  17. package/dist/core/config/content_loader.js +57 -205
  18. package/dist/core/config/content_loader.js.map +1 -1
  19. package/dist/core/config/markdown_frontmatter.js +34 -0
  20. package/dist/core/config/markdown_frontmatter.js.map +1 -0
  21. package/dist/core/config/schema.js +266 -332
  22. package/dist/core/config/schema.js.map +1 -1
  23. package/dist/core/config/skill_parser.js +8 -32
  24. package/dist/core/config/skill_parser.js.map +1 -1
  25. package/dist/core/config/skills_loader.js +32 -18
  26. package/dist/core/config/skills_loader.js.map +1 -1
  27. package/dist/core/events/index.js +1 -0
  28. package/dist/core/events/index.js.map +1 -1
  29. package/dist/core/events/parser.js +115 -0
  30. package/dist/core/events/parser.js.map +1 -0
  31. package/dist/core/index.js +1 -1
  32. package/dist/core/index.js.map +1 -1
  33. package/dist/core/modes/rpc_protocol.js +249 -189
  34. package/dist/core/modes/rpc_protocol.js.map +1 -1
  35. package/dist/core/session/session_engine.js +1 -3
  36. package/dist/core/session/session_engine.js.map +1 -1
  37. package/dist/core/subagents/control_plane.js +13 -2
  38. package/dist/core/subagents/control_plane.js.map +1 -1
  39. package/dist/core/subagents/subagent_engine.js +3 -3
  40. package/dist/core/subagents/subagent_engine.js.map +1 -1
  41. package/dist/core/tools/emit_output.js +3 -2
  42. package/dist/core/tools/emit_output.js.map +1 -1
  43. package/dist/core/tools/registry.js +6 -0
  44. package/dist/core/tools/registry.js.map +1 -1
  45. package/dist/core/tools/send_input_to_agent.js +12 -16
  46. package/dist/core/tools/send_input_to_agent.js.map +1 -1
  47. package/dist/core/tools/spawn_agent.js +25 -30
  48. package/dist/core/tools/spawn_agent.js.map +1 -1
  49. package/dist/core/tools/terminate_agent.js +10 -14
  50. package/dist/core/tools/terminate_agent.js.map +1 -1
  51. package/dist/core/tools/wait_for_agent.js +10 -14
  52. package/dist/core/tools/wait_for_agent.js.map +1 -1
  53. package/dist/core/utils/mistral_transcription.js +13 -21
  54. package/dist/core/utils/mistral_transcription.js.map +1 -1
  55. package/dist/core/utils/parallel_api.js +15 -19
  56. package/dist/core/utils/parallel_api.js.map +1 -1
  57. package/dist/core/utils/zod.js +7 -0
  58. package/dist/core/utils/zod.js.map +1 -1
  59. package/dist/core/version.js +1 -1
  60. package/dist/tui/chat_controller/session_maintenance_service.js +98 -171
  61. package/dist/tui/chat_controller/session_maintenance_service.js.map +1 -1
  62. package/package.json +1 -1
@@ -1,14 +1,78 @@
1
1
  import { resolve } from "node:path";
2
2
  import { getModels, getProviders } from "@mariozechner/pi-ai";
3
+ import { z } from "zod";
3
4
  import { formatPersonaReference, parsePersonaReference } from "../persona_reference.js";
4
5
  import { parseSubagentLaunchModelList } from "../subagents/launch_model.js";
5
6
  import { REASONING_LEVELS, RiskLevelSchema } from "../types.js";
6
7
  import { normalizeModelNoticeKey, parseModelNoticeKey } from "../utils/model_notices.js";
7
- import { isRecord } from "../utils/type_guards.js";
8
8
  import { parseBashCommands } from "./bash_commands.js";
9
9
  import { createDefaultConfigDeps } from "./deps.js";
10
10
  import { resolveConfigLevels } from "./paths.js";
11
11
  import { getVirtualConfigDefaults } from "./virtual_defaults.js";
12
+ const NonEmptyStringSchema = z.string().trim().min(1);
13
+ const BooleanSchema = z.boolean();
14
+ const AgentContextFilesSchema = z.array(NonEmptyStringSchema);
15
+ const ApiKeyProviderSchema = z.string();
16
+ const ApiKeysSchema = z
17
+ .object({
18
+ anthropic: z.unknown().optional(),
19
+ google: z.unknown().optional(),
20
+ openai: z.unknown().optional(),
21
+ parallel: z.unknown().optional(),
22
+ mistral: z.unknown().optional(),
23
+ })
24
+ .passthrough();
25
+ const SandboxSchema = z
26
+ .object({
27
+ image: z.unknown().optional(),
28
+ mountPath: z.unknown().optional(),
29
+ pruneAfterHours: z.unknown().optional(),
30
+ extraDockerArgs: z.unknown().optional(),
31
+ environmentInfo: z.unknown().optional(),
32
+ })
33
+ .passthrough();
34
+ const SandboxFieldsSchema = z.object({
35
+ image: NonEmptyStringSchema,
36
+ mountPath: NonEmptyStringSchema,
37
+ pruneAfterHours: z.number().finite().gt(0),
38
+ extraDockerArgs: z.array(z.string().refine((entry) => entry.trim().length > 0)),
39
+ environmentInfo: z.string(),
40
+ });
41
+ const SubagentsConfigSchema = z
42
+ .object({
43
+ defaultLaunchModels: z.array(z.string()).optional(),
44
+ })
45
+ .passthrough();
46
+ const StringRecordSchema = z.object({}).catchall(z.unknown());
47
+ const PositiveIntegerSchema = z.number().int().finite().gt(0);
48
+ const AsyncClientTargetSchema = z.object({
49
+ url: NonEmptyStringSchema,
50
+ token: NonEmptyStringSchema,
51
+ timeoutMs: PositiveIntegerSchema.optional(),
52
+ });
53
+ function parseOptionalFields(data, sourceLabel, specs) {
54
+ const values = {};
55
+ const errors = [];
56
+ for (const [key, schema, errorMessage] of specs) {
57
+ const value = data[key];
58
+ if (value === undefined) {
59
+ continue;
60
+ }
61
+ const parsed = schema.safeParse(value);
62
+ if (!parsed.success) {
63
+ errors.push(`${sourceLabel}: ${errorMessage}`);
64
+ continue;
65
+ }
66
+ values[key] = parsed.data;
67
+ }
68
+ return { values, errors };
69
+ }
70
+ function assignParsedConfigValue(config, errors, key, value, parseErrors) {
71
+ if (value !== undefined) {
72
+ config[key] = value;
73
+ }
74
+ errors.push(...parseErrors);
75
+ }
12
76
  function parseConfigJson(content, sourceLabel) {
13
77
  try {
14
78
  return { data: JSON.parse(content), errors: [] };
@@ -21,250 +85,179 @@ function parseConfigJson(content, sourceLabel) {
21
85
  };
22
86
  }
23
87
  }
24
- function validateConfigData(raw, sourceLabel) {
25
- if (typeof raw !== "object" || raw === null) {
26
- return { config: {}, errors: [`${sourceLabel}: config must be an object.`] };
88
+ function parseApiKeysConfig(raw, sourceLabel) {
89
+ if (raw === undefined) {
90
+ return { errors: [] };
27
91
  }
28
- const data = raw;
29
- const config = {};
92
+ const parsed = ApiKeysSchema.safeParse(raw);
93
+ if (!parsed.success) {
94
+ return { errors: [`${sourceLabel}: 'apiKeys' must be an object.`] };
95
+ }
96
+ const apiKeys = {};
30
97
  const errors = [];
31
- if (data.apiKeys !== undefined) {
32
- if (typeof data.apiKeys !== "object" || data.apiKeys === null) {
33
- errors.push(`${sourceLabel}: 'apiKeys' must be an object.`);
98
+ for (const provider of ["anthropic", "google", "openai", "parallel", "mistral"]) {
99
+ const value = parsed.data[provider];
100
+ if (value === undefined) {
101
+ continue;
34
102
  }
35
- else {
36
- const apiKeys = {};
37
- const keys = data.apiKeys;
38
- const providers = ["anthropic", "google", "openai", "parallel", "mistral"];
39
- for (const provider of providers) {
40
- const value = keys[provider];
41
- if (value === undefined)
42
- continue;
43
- if (typeof value !== "string") {
44
- errors.push(`${sourceLabel}: apiKeys.${provider} must be a string.`);
45
- continue;
46
- }
47
- apiKeys[provider] = value;
48
- }
49
- if (Object.keys(apiKeys).length > 0) {
50
- config.apiKeys = apiKeys;
51
- }
103
+ const parsedValue = ApiKeyProviderSchema.safeParse(value);
104
+ if (!parsedValue.success) {
105
+ errors.push(`${sourceLabel}: apiKeys.${provider} must be a string.`);
106
+ continue;
52
107
  }
108
+ apiKeys[provider] = parsedValue.data;
53
109
  }
54
- const sandboxResult = parseSandboxConfig(data.sandbox, sourceLabel);
55
- if (sandboxResult.config) {
56
- config.sandbox = sandboxResult.config;
57
- }
58
- errors.push(...sandboxResult.errors);
59
- if (data.defaultPersona !== undefined) {
60
- if (typeof data.defaultPersona === "string") {
61
- const parsedDefaultPersona = parsePersonaReference(data.defaultPersona);
62
- if (parsedDefaultPersona.error === "empty-persona") {
63
- errors.push(`${sourceLabel}: 'defaultPersona' must be a non-empty string.`);
64
- }
65
- else if (parsedDefaultPersona.error === "missing-reasoning") {
66
- errors.push(`${sourceLabel}: 'defaultPersona' is missing a reasoning level after ':'.`);
67
- }
68
- else if (parsedDefaultPersona.error === "invalid-reasoning") {
69
- errors.push(`${sourceLabel}: 'defaultPersona' has invalid reasoning level '${parsedDefaultPersona.rawReasoning}'. allowed levels: ${REASONING_LEVELS.join(", ")}.`);
70
- }
71
- else if (parsedDefaultPersona.personaId) {
72
- config.defaultPersona = formatPersonaReference({
73
- personaId: parsedDefaultPersona.personaId,
74
- reasoning: parsedDefaultPersona.reasoning,
75
- });
76
- }
77
- }
78
- else {
79
- errors.push(`${sourceLabel}: 'defaultPersona' must be a string.`);
80
- }
110
+ if (Object.keys(apiKeys).length === 0) {
111
+ return { errors };
81
112
  }
82
- if (data.defaultRisk !== undefined) {
83
- const parsed = RiskLevelSchema.safeParse(data.defaultRisk);
84
- if (parsed.success) {
85
- config.defaultRisk = parsed.data;
86
- }
87
- else {
88
- errors.push(`${sourceLabel}: 'defaultRisk' must be a valid risk level.`);
89
- }
113
+ return { config: apiKeys, errors };
114
+ }
115
+ function parseDefaultPersona(raw, sourceLabel) {
116
+ if (raw === undefined) {
117
+ return { errors: [] };
90
118
  }
91
- if (data.disableBuiltinPersonas !== undefined) {
92
- if (typeof data.disableBuiltinPersonas === "boolean") {
93
- config.disableBuiltinPersonas = data.disableBuiltinPersonas;
94
- }
95
- else {
96
- errors.push(`${sourceLabel}: 'disableBuiltinPersonas' must be a boolean.`);
97
- }
119
+ const parsedPersona = z.string().safeParse(raw);
120
+ if (!parsedPersona.success) {
121
+ return { errors: [`${sourceLabel}: 'defaultPersona' must be a string.`] };
98
122
  }
99
- if (data.disableBuiltinThemes !== undefined) {
100
- if (typeof data.disableBuiltinThemes === "boolean") {
101
- config.disableBuiltinThemes = data.disableBuiltinThemes;
102
- }
103
- else {
104
- errors.push(`${sourceLabel}: 'disableBuiltinThemes' must be a boolean.`);
105
- }
123
+ const ref = parsePersonaReference(parsedPersona.data);
124
+ if (ref.personaId && !ref.error) {
125
+ return {
126
+ defaultPersona: formatPersonaReference({
127
+ personaId: ref.personaId,
128
+ reasoning: ref.reasoning,
129
+ }),
130
+ errors: [],
131
+ };
106
132
  }
107
- if (data.defaultTheme !== undefined) {
108
- if (typeof data.defaultTheme === "string") {
109
- const defaultTheme = data.defaultTheme.trim();
110
- if (!defaultTheme) {
111
- errors.push(`${sourceLabel}: 'defaultTheme' must be a non-empty string.`);
112
- }
113
- else {
114
- config.defaultTheme = defaultTheme;
115
- }
116
- }
117
- else {
118
- errors.push(`${sourceLabel}: 'defaultTheme' must be a string.`);
119
- }
133
+ if (ref.error === "empty-persona") {
134
+ return { errors: [`${sourceLabel}: 'defaultPersona' must be a non-empty string.`] };
120
135
  }
121
- const bashResult = parseBashCommands(data.bashCommands, sourceLabel);
122
- if (bashResult.commands.length > 0) {
123
- config.bashCommands = bashResult.commands;
136
+ if (ref.error === "missing-reasoning") {
137
+ return {
138
+ errors: [`${sourceLabel}: 'defaultPersona' is missing a reasoning level after ':'.`],
139
+ };
124
140
  }
125
- errors.push(...bashResult.errors);
126
- const agentResult = parseAgentContextFiles(data.agentContextFiles, sourceLabel);
127
- if (agentResult.paths.length > 0) {
128
- config.agentContextFiles = agentResult.paths;
141
+ if (ref.error === "invalid-reasoning") {
142
+ return {
143
+ errors: [
144
+ `${sourceLabel}: 'defaultPersona' has invalid reasoning level '${ref.rawReasoning}'. allowed levels: ${REASONING_LEVELS.join(", ")}.`,
145
+ ],
146
+ };
129
147
  }
130
- errors.push(...agentResult.errors);
131
- const subagentsResult = parseSubagentsConfig(data.subagents, sourceLabel);
132
- if (subagentsResult.config) {
133
- config.subagents = subagentsResult.config;
148
+ return { errors: [] };
149
+ }
150
+ function validateConfigData(raw, sourceLabel) {
151
+ if (typeof raw !== "object" || raw === null) {
152
+ return { config: {}, errors: [`${sourceLabel}: config must be an object.`] };
134
153
  }
135
- errors.push(...subagentsResult.errors);
154
+ const data = raw;
155
+ const config = {};
156
+ const errors = [];
157
+ const apiKeysResult = parseApiKeysConfig(data.apiKeys, sourceLabel);
158
+ assignParsedConfigValue(config, errors, "apiKeys", apiKeysResult.config, apiKeysResult.errors);
159
+ const sandboxResult = parseSandboxConfig(data.sandbox, sourceLabel);
160
+ assignParsedConfigValue(config, errors, "sandbox", sandboxResult.config, sandboxResult.errors);
161
+ const defaultPersonaResult = parseDefaultPersona(data.defaultPersona, sourceLabel);
162
+ assignParsedConfigValue(config, errors, "defaultPersona", defaultPersonaResult.defaultPersona, defaultPersonaResult.errors);
163
+ const scalarResult = parseOptionalFields(data, sourceLabel, [
164
+ ["defaultRisk", RiskLevelSchema, "'defaultRisk' must be a valid risk level."],
165
+ ["disableBuiltinPersonas", BooleanSchema, "'disableBuiltinPersonas' must be a boolean."],
166
+ ["disableBuiltinThemes", BooleanSchema, "'disableBuiltinThemes' must be a boolean."],
167
+ ["defaultTheme", NonEmptyStringSchema, "'defaultTheme' must be a non-empty string."],
168
+ ]);
169
+ Object.assign(config, scalarResult.values);
170
+ errors.push(...scalarResult.errors);
171
+ const bashResult = parseBashCommands(data.bashCommands, sourceLabel);
172
+ assignParsedConfigValue(config, errors, "bashCommands", bashResult.commands.length > 0 ? bashResult.commands : undefined, bashResult.errors);
173
+ const agentResult = parseAgentContextFiles(data.agentContextFiles, sourceLabel);
174
+ assignParsedConfigValue(config, errors, "agentContextFiles", agentResult.paths.length > 0 ? agentResult.paths : undefined, agentResult.errors);
175
+ const subagentsResult = parseSubagentsConfig(data.subagents, sourceLabel);
176
+ assignParsedConfigValue(config, errors, "subagents", subagentsResult.config, subagentsResult.errors);
136
177
  const modelSystemNoticesResult = parseModelSystemNotices(data.modelSystemNotices, sourceLabel);
137
- if (modelSystemNoticesResult.notices) {
138
- config.modelSystemNotices = modelSystemNoticesResult.notices;
139
- }
140
- errors.push(...modelSystemNoticesResult.errors);
178
+ assignParsedConfigValue(config, errors, "modelSystemNotices", modelSystemNoticesResult.notices, modelSystemNoticesResult.errors);
141
179
  const asyncResult = parseAsyncConfig(data.async, sourceLabel);
142
- if (asyncResult.config) {
143
- config.async = asyncResult.config;
144
- }
145
- errors.push(...asyncResult.errors);
180
+ assignParsedConfigValue(config, errors, "async", asyncResult.config, asyncResult.errors);
146
181
  return { config, errors };
147
182
  }
148
183
  function parseSandboxConfig(raw, sourceLabel) {
149
184
  if (raw === undefined) {
150
185
  return { errors: [] };
151
186
  }
152
- if (typeof raw !== "object" || raw === null) {
187
+ const parsed = SandboxSchema.safeParse(raw);
188
+ if (!parsed.success) {
153
189
  return { errors: [`${sourceLabel}: 'sandbox' must be an object.`] };
154
190
  }
155
- const data = raw;
156
- const errors = [];
157
- const sandbox = {};
158
- if (data.image !== undefined) {
159
- if (typeof data.image === "string" && data.image.trim()) {
160
- sandbox.image = data.image.trim();
161
- }
162
- else {
163
- errors.push(`${sourceLabel}: sandbox.image must be a non-empty string.`);
164
- }
165
- }
166
- if (data.mountPath !== undefined) {
167
- if (typeof data.mountPath === "string" && data.mountPath.trim()) {
168
- sandbox.mountPath = data.mountPath.trim();
169
- }
170
- else {
171
- errors.push(`${sourceLabel}: sandbox.mountPath must be a non-empty string.`);
172
- }
173
- }
174
- if (data.pruneAfterHours !== undefined) {
175
- if (typeof data.pruneAfterHours === "number" &&
176
- Number.isFinite(data.pruneAfterHours) &&
177
- data.pruneAfterHours > 0) {
178
- sandbox.pruneAfterHours = data.pruneAfterHours;
179
- }
180
- else {
181
- errors.push(`${sourceLabel}: sandbox.pruneAfterHours must be a positive number.`);
182
- }
183
- }
184
- if (data.extraDockerArgs !== undefined) {
185
- if (Array.isArray(data.extraDockerArgs)) {
186
- const args = [];
187
- let invalid = false;
188
- for (const entry of data.extraDockerArgs) {
189
- if (typeof entry !== "string" || !entry.trim()) {
190
- invalid = true;
191
- continue;
192
- }
193
- args.push(entry);
194
- }
195
- if (invalid) {
196
- errors.push(`${sourceLabel}: sandbox.extraDockerArgs must be a string array.`);
197
- }
198
- else {
199
- sandbox.extraDockerArgs = args;
200
- }
201
- }
202
- else {
203
- errors.push(`${sourceLabel}: sandbox.extraDockerArgs must be a string array.`);
204
- }
205
- }
206
- if (data.environmentInfo !== undefined) {
207
- if (typeof data.environmentInfo === "string") {
208
- sandbox.environmentInfo = data.environmentInfo;
209
- }
210
- else {
211
- errors.push(`${sourceLabel}: sandbox.environmentInfo must be a string.`);
212
- }
213
- }
214
- if (Object.keys(sandbox).length === 0) {
215
- return { errors };
216
- }
217
- return { config: sandbox, errors };
191
+ const sandboxResult = parseOptionalFields(parsed.data, sourceLabel, [
192
+ ["image", SandboxFieldsSchema.shape.image, "sandbox.image must be a non-empty string."],
193
+ [
194
+ "mountPath",
195
+ SandboxFieldsSchema.shape.mountPath,
196
+ "sandbox.mountPath must be a non-empty string.",
197
+ ],
198
+ [
199
+ "pruneAfterHours",
200
+ SandboxFieldsSchema.shape.pruneAfterHours,
201
+ "sandbox.pruneAfterHours must be a positive number.",
202
+ ],
203
+ [
204
+ "extraDockerArgs",
205
+ SandboxFieldsSchema.shape.extraDockerArgs,
206
+ "sandbox.extraDockerArgs must be a string array.",
207
+ ],
208
+ [
209
+ "environmentInfo",
210
+ SandboxFieldsSchema.shape.environmentInfo,
211
+ "sandbox.environmentInfo must be a string.",
212
+ ],
213
+ ]);
214
+ if (Object.keys(sandboxResult.values).length === 0) {
215
+ return { errors: sandboxResult.errors };
216
+ }
217
+ return { config: sandboxResult.values, errors: sandboxResult.errors };
218
218
  }
219
219
  function parseAgentContextFiles(raw, sourceLabel) {
220
220
  if (raw === undefined) {
221
221
  return { paths: [], errors: [] };
222
222
  }
223
- if (!Array.isArray(raw)) {
223
+ const parsed = AgentContextFilesSchema.safeParse(raw);
224
+ if (!parsed.success) {
224
225
  return {
225
226
  paths: [],
226
227
  errors: [`${sourceLabel}: 'agentContextFiles' must be a string array.`],
227
228
  };
228
229
  }
229
- const paths = [];
230
- for (const entry of raw) {
231
- if (typeof entry !== "string" || !entry.trim()) {
232
- return {
233
- paths: [],
234
- errors: [`${sourceLabel}: 'agentContextFiles' must be a string array.`],
235
- };
236
- }
237
- paths.push(entry.trim());
238
- }
239
- return { paths, errors: [] };
230
+ return { paths: parsed.data, errors: [] };
240
231
  }
241
232
  function parseSubagentsConfig(raw, sourceLabel) {
242
233
  if (raw === undefined) {
243
234
  return { errors: [] };
244
235
  }
245
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
236
+ const parsed = SubagentsConfigSchema.safeParse(raw);
237
+ if (!parsed.success) {
238
+ if (parsed.error.issues.some((issue) => issue.path[0] === "defaultLaunchModels")) {
239
+ return { errors: [`${sourceLabel}: subagents.defaultLaunchModels must be a string array.`] };
240
+ }
246
241
  return { errors: [`${sourceLabel}: 'subagents' must be an object.`] };
247
242
  }
248
- const data = raw;
249
- const config = {};
250
- const errors = [];
251
- if ("defaultLaunchModels" in data) {
252
- if (!Array.isArray(data.defaultLaunchModels)) {
253
- errors.push(`${sourceLabel}: subagents.defaultLaunchModels must be a string array.`);
254
- return { errors };
255
- }
256
- const launchModelsResult = parseSubagentLaunchModelList(data.defaultLaunchModels);
257
- if (launchModelsResult.error) {
258
- errors.push(`${sourceLabel}: subagents.defaultLaunchModels ${launchModelsResult.error}. expected <provider>/<model>:<effort>.`);
259
- }
260
- else {
261
- config.defaultLaunchModels = launchModelsResult.launchModels;
262
- }
243
+ const { defaultLaunchModels } = parsed.data;
244
+ if (defaultLaunchModels === undefined) {
245
+ return { errors: [] };
263
246
  }
264
- if (Object.keys(config).length === 0) {
265
- return { errors };
247
+ const launchModelsResult = parseSubagentLaunchModelList(defaultLaunchModels);
248
+ if (launchModelsResult.error) {
249
+ return {
250
+ errors: [
251
+ `${sourceLabel}: subagents.defaultLaunchModels ${launchModelsResult.error}. expected <provider>/<model>:<effort>.`,
252
+ ],
253
+ };
266
254
  }
267
- return { config, errors };
255
+ return {
256
+ config: {
257
+ defaultLaunchModels: launchModelsResult.launchModels,
258
+ },
259
+ errors: [],
260
+ };
268
261
  }
269
262
  function parseModelNoticeTarget(rawKey) {
270
263
  const parsedKey = parseModelNoticeKey(rawKey);
@@ -289,113 +282,99 @@ function parseModelSystemNotices(raw, sourceLabel) {
289
282
  if (raw === undefined) {
290
283
  return { errors: [] };
291
284
  }
292
- if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
285
+ const parsedRecord = StringRecordSchema.safeParse(raw);
286
+ if (!parsedRecord.success) {
293
287
  return { errors: [`${sourceLabel}: 'modelSystemNotices' must be an object.`] };
294
288
  }
295
- const data = raw;
296
289
  const notices = {};
297
290
  const errors = [];
298
- for (const [rawKey, rawValue] of Object.entries(data)) {
291
+ for (const [rawKey, rawValue] of Object.entries(parsedRecord.data)) {
299
292
  const parsedKey = parseModelNoticeTarget(rawKey);
300
293
  if (parsedKey.error || !parsedKey.normalizedKey) {
301
294
  errors.push(`${sourceLabel}: modelSystemNotices.${rawKey} ${parsedKey.error ?? "is invalid"}.`);
302
295
  continue;
303
296
  }
304
- if (typeof rawValue !== "string" || !rawValue.trim()) {
297
+ const parsedNotice = NonEmptyStringSchema.safeParse(rawValue);
298
+ if (!parsedNotice.success) {
305
299
  errors.push(`${sourceLabel}: modelSystemNotices.${rawKey} must be a non-empty string notice.`);
306
300
  continue;
307
301
  }
308
- notices[parsedKey.normalizedKey] = rawValue.trim();
302
+ notices[parsedKey.normalizedKey] = parsedNotice.data;
309
303
  }
310
304
  if (Object.keys(notices).length === 0) {
311
305
  return { errors };
312
306
  }
313
307
  return { notices, errors };
314
308
  }
315
- function isPositiveInteger(value) {
316
- return (typeof value === "number" && Number.isInteger(value) && Number.isFinite(value) && value > 0);
317
- }
318
- function parsePositiveIntegerField(value, options) {
319
- if (!isPositiveInteger(value)) {
320
- return undefined;
321
- }
322
- if (options?.max !== undefined && value > options.max) {
323
- return undefined;
324
- }
325
- return value;
326
- }
327
309
  function parseAsyncClientTarget(raw, sourceLabel, key) {
328
- if (!isRecord(raw)) {
310
+ const parsedRecord = StringRecordSchema.safeParse(raw);
311
+ if (!parsedRecord.success) {
329
312
  return { errors: [`${sourceLabel}: async.client.targets.${key} must be an object.`] };
330
313
  }
331
- const data = raw;
332
- const errors = [];
333
- if (typeof data.url !== "string" || !data.url.trim()) {
334
- errors.push(`${sourceLabel}: async.client.targets.${key}.url must be a non-empty string.`);
335
- }
336
- if (typeof data.token !== "string" || !data.token.trim()) {
337
- errors.push(`${sourceLabel}: async.client.targets.${key}.token must be a non-empty string.`);
338
- }
339
- const config = {
340
- url: typeof data.url === "string" ? data.url.trim() : "",
341
- token: typeof data.token === "string" ? data.token.trim() : "",
342
- };
343
- if (data.timeoutMs !== undefined) {
344
- const parsed = parsePositiveIntegerField(data.timeoutMs);
345
- if (parsed !== undefined) {
346
- config.timeoutMs = parsed;
347
- }
348
- else {
349
- errors.push(`${sourceLabel}: async.client.targets.${key}.timeoutMs must be a positive integer.`);
350
- }
351
- }
352
- if (errors.length > 0) {
353
- return { errors };
354
- }
355
- return { config, errors: [] };
314
+ const parsedTarget = AsyncClientTargetSchema.safeParse(parsedRecord.data);
315
+ if (parsedTarget.success) {
316
+ return { config: parsedTarget.data, errors: [] };
317
+ }
318
+ const errors = new Set();
319
+ for (const issue of parsedTarget.error.issues) {
320
+ switch (issue.path[0]) {
321
+ case "url":
322
+ errors.add(`${sourceLabel}: async.client.targets.${key}.url must be a non-empty string.`);
323
+ break;
324
+ case "token":
325
+ errors.add(`${sourceLabel}: async.client.targets.${key}.token must be a non-empty string.`);
326
+ break;
327
+ case "timeoutMs":
328
+ errors.add(`${sourceLabel}: async.client.targets.${key}.timeoutMs must be a positive integer.`);
329
+ break;
330
+ default:
331
+ break;
332
+ }
333
+ }
334
+ return { errors: [...errors] };
356
335
  }
357
336
  function parseAsyncClientConfig(raw, sourceLabel) {
358
337
  if (raw === undefined) {
359
338
  return { errors: [] };
360
339
  }
361
- if (!isRecord(raw)) {
340
+ const parsedRecord = StringRecordSchema.safeParse(raw);
341
+ if (!parsedRecord.success) {
362
342
  return { errors: [`${sourceLabel}: async.client must be an object.`] };
363
343
  }
364
- const data = raw;
365
- const errors = [];
344
+ const data = parsedRecord.data;
366
345
  const config = {};
367
- if (data.defaultTarget !== undefined) {
368
- if (typeof data.defaultTarget === "string" && data.defaultTarget.trim()) {
369
- config.defaultTarget = data.defaultTarget.trim();
370
- }
371
- else {
372
- errors.push(`${sourceLabel}: async.client.defaultTarget must be a non-empty string.`);
373
- }
374
- }
375
- if (data.defaultProjectId !== undefined) {
376
- if (typeof data.defaultProjectId === "string" && data.defaultProjectId.trim()) {
377
- config.defaultProjectId = data.defaultProjectId.trim();
378
- }
379
- else {
380
- errors.push(`${sourceLabel}: async.client.defaultProjectId must be a non-empty string.`);
381
- }
382
- }
346
+ const scalarResult = parseOptionalFields(data, sourceLabel, [
347
+ [
348
+ "defaultTarget",
349
+ NonEmptyStringSchema,
350
+ "async.client.defaultTarget must be a non-empty string.",
351
+ ],
352
+ [
353
+ "defaultProjectId",
354
+ NonEmptyStringSchema,
355
+ "async.client.defaultProjectId must be a non-empty string.",
356
+ ],
357
+ ]);
358
+ Object.assign(config, scalarResult.values);
359
+ const errors = [...scalarResult.errors];
383
360
  if (data.targets !== undefined) {
384
- if (!isRecord(data.targets)) {
361
+ const parsedTargetsRecord = StringRecordSchema.safeParse(data.targets);
362
+ if (!parsedTargetsRecord.success) {
385
363
  errors.push(`${sourceLabel}: async.client.targets must be an object.`);
386
364
  }
387
365
  else {
388
366
  const targets = {};
389
- for (const [key, value] of Object.entries(data.targets)) {
390
- if (!key.trim()) {
367
+ for (const [key, value] of Object.entries(parsedTargetsRecord.data)) {
368
+ const parsedTargetKey = NonEmptyStringSchema.safeParse(key);
369
+ if (!parsedTargetKey.success) {
391
370
  errors.push(`${sourceLabel}: async.client.targets keys must be non-empty.`);
392
371
  continue;
393
372
  }
394
- const parsed = parseAsyncClientTarget(value, sourceLabel, key);
395
- if (parsed.config) {
396
- targets[key] = parsed.config;
373
+ const parsedTarget = parseAsyncClientTarget(value, sourceLabel, key);
374
+ if (parsedTarget.config) {
375
+ targets[key] = parsedTarget.config;
397
376
  }
398
- errors.push(...parsed.errors);
377
+ errors.push(...parsedTarget.errors);
399
378
  }
400
379
  if (Object.keys(targets).length > 0) {
401
380
  config.targets = targets;
@@ -411,10 +390,11 @@ function parseAsyncConfig(raw, sourceLabel) {
411
390
  if (raw === undefined) {
412
391
  return { errors: [] };
413
392
  }
414
- if (!isRecord(raw)) {
393
+ const parsedRecord = StringRecordSchema.safeParse(raw);
394
+ if (!parsedRecord.success) {
415
395
  return { errors: [`${sourceLabel}: 'async' must be an object.`] };
416
396
  }
417
- const data = raw;
397
+ const data = parsedRecord.data;
418
398
  const config = {};
419
399
  const errors = [];
420
400
  const clientResult = parseAsyncClientConfig(data.client, sourceLabel);
@@ -422,11 +402,12 @@ function parseAsyncConfig(raw, sourceLabel) {
422
402
  config.client = clientResult.config;
423
403
  }
424
404
  errors.push(...clientResult.errors);
425
- if (data.server !== undefined) {
426
- errors.push(`${sourceLabel}: async.server was moved to daemon config file. use 'tau async daemon --config-file <path>'.`);
427
- }
428
- if (data.projects !== undefined) {
429
- errors.push(`${sourceLabel}: async.projects was moved to daemon config file. use 'tau async daemon --config-file <path>'.`);
405
+ const movedAsyncFields = ["server", "projects"];
406
+ for (const field of movedAsyncFields) {
407
+ if (data[field] === undefined) {
408
+ continue;
409
+ }
410
+ errors.push(`${sourceLabel}: async.${field} was moved to daemon config file. use 'tau async daemon --config-file <path>'.`);
430
411
  }
431
412
  if (Object.keys(config).length === 0) {
432
413
  return { errors };
@@ -455,16 +436,7 @@ function loadConfigFile(level, deps, sourceLabel) {
455
436
  };
456
437
  }
457
438
  }
458
- function mergeApiKeys(target, overlay) {
459
- if (!target && !overlay) {
460
- return undefined;
461
- }
462
- return {
463
- ...(target ?? {}),
464
- ...(overlay ?? {}),
465
- };
466
- }
467
- function mergeSandboxConfig(target, overlay) {
439
+ function mergeOptionalObject(target, overlay) {
468
440
  if (!target && !overlay) {
469
441
  return undefined;
470
442
  }
@@ -485,24 +457,6 @@ function mergeSubagentsConfig(target, overlay) {
485
457
  }
486
458
  return merged;
487
459
  }
488
- function mergeModelSystemNotices(target, overlay) {
489
- if (!target && !overlay) {
490
- return undefined;
491
- }
492
- return {
493
- ...(target ?? {}),
494
- ...(overlay ?? {}),
495
- };
496
- }
497
- function mergeAsyncClientTarget(target, overlay) {
498
- if (!target && !overlay) {
499
- return undefined;
500
- }
501
- return {
502
- ...(target ?? {}),
503
- ...(overlay ?? {}),
504
- };
505
- }
506
460
  function mergeAsyncClientConfig(target, overlay) {
507
461
  if (!target && !overlay) {
508
462
  return undefined;
@@ -522,7 +476,7 @@ function mergeAsyncClientConfig(target, overlay) {
522
476
  targets.set(key, { ...value });
523
477
  }
524
478
  for (const [key, value] of Object.entries(overlay?.targets ?? {})) {
525
- targets.set(key, mergeAsyncClientTarget(targets.get(key), value) ?? value);
479
+ targets.set(key, mergeOptionalObject(targets.get(key), value) ?? value);
526
480
  }
527
481
  if (targets.size > 0) {
528
482
  merged.targets = Object.fromEntries(targets.entries());
@@ -541,26 +495,6 @@ function mergeAsyncConfig(target, overlay) {
541
495
  };
542
496
  return merged;
543
497
  }
544
- function resolveAsyncConfig(_level, config) {
545
- return {
546
- ...config,
547
- ...(config.client
548
- ? {
549
- client: {
550
- ...config.client,
551
- ...(config.client.targets
552
- ? {
553
- targets: Object.fromEntries(Object.entries(config.client.targets).map(([key, value]) => [
554
- key,
555
- { ...value },
556
- ])),
557
- }
558
- : {}),
559
- },
560
- }
561
- : {}),
562
- };
563
- }
564
498
  function resolveAgentContextPaths(level, rawPaths) {
565
499
  const root = level.levelRoot;
566
500
  return rawPaths.map((entry) => resolve(root, entry));
@@ -592,11 +526,11 @@ function mergeConfigLevels(levels, configs) {
592
526
  for (let i = 0; i < levels.length; i += 1) {
593
527
  const level = levels[i];
594
528
  const config = configs[i] ?? {};
595
- apiKeys = mergeApiKeys(apiKeys, config.apiKeys);
596
- sandbox = mergeSandboxConfig(sandbox, config.sandbox);
529
+ apiKeys = mergeOptionalObject(apiKeys, config.apiKeys);
530
+ sandbox = mergeOptionalObject(sandbox, config.sandbox);
597
531
  subagents = mergeSubagentsConfig(subagents, config.subagents);
598
- modelSystemNotices = mergeModelSystemNotices(modelSystemNotices, config.modelSystemNotices);
599
- asyncConfig = mergeAsyncConfig(asyncConfig, config.async ? resolveAsyncConfig(level, config.async) : undefined);
532
+ modelSystemNotices = mergeOptionalObject(modelSystemNotices, config.modelSystemNotices);
533
+ asyncConfig = mergeAsyncConfig(asyncConfig, config.async);
600
534
  if (config.defaultPersona !== undefined) {
601
535
  merged.defaultPersona = config.defaultPersona;
602
536
  }