@markusylisiurunen/tau 0.2.62 → 0.2.64

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