Migrate monorepo to Zod 4

This commit is contained in:
Mohamed Boudra
2026-06-18 15:02:42 +08:00
committed by GitHub
parent de84c8f179
commit df18bbc43a
34 changed files with 5233 additions and 4969 deletions
+2283 -2984
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -109,7 +109,7 @@
"react-native-worklets": "~0.8.3", "react-native-worklets": "~0.8.3",
"tiny-invariant": "^1.3.3", "tiny-invariant": "^1.3.3",
"use-sync-external-store": "^1.6.0", "use-sync-external-store": "^1.6.0",
"zod": "^3.23.8", "zod": "^4.4.3",
"zustand": "^5.0.9" "zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {
@@ -18,13 +18,13 @@ export interface FavoriteModelRow {
const providerPreferencesSchema = z.object({ const providerPreferencesSchema = z.object({
model: z.string().optional(), model: z.string().optional(),
mode: z.string().optional(), mode: z.string().optional(),
thinkingByModel: z.record(z.string()).optional(), thinkingByModel: z.record(z.string(), z.string()).optional(),
featureValues: z.record(z.unknown()).optional(), featureValues: z.record(z.string(), z.unknown()).optional(),
}); });
const formPreferencesSchema = z.object({ const formPreferencesSchema = z.object({
provider: z.string().optional(), provider: z.string().optional(),
providerPreferences: z.record(providerPreferencesSchema).optional(), providerPreferences: z.record(z.string(), providerPreferencesSchema).optional(),
favoriteModels: z favoriteModels: z
.array( .array(
z.object({ z.object({
+1 -1
View File
@@ -37,7 +37,7 @@
"dependencies": { "dependencies": {
"@getpaseo/protocol": "0.1.97-beta.3", "@getpaseo/protocol": "0.1.97-beta.3",
"@getpaseo/relay": "0.1.97-beta.3", "@getpaseo/relay": "0.1.97-beta.3",
"zod": "^3.23.8" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.9.0", "@types/node": "^20.9.0",
+1 -1
View File
@@ -116,7 +116,7 @@ function parseSentFrame(
return z return z
.object({ .object({
type: z.literal("session"), type: z.literal("session"),
message: z.record(z.unknown()), message: z.record(z.string(), z.unknown()),
}) })
.parse(JSON.parse(assertStr(data))).message; .parse(JSON.parse(assertStr(data))).message;
} }
+1 -1
View File
@@ -28,7 +28,7 @@
"electron-log": "^5.4.3", "electron-log": "^5.4.3",
"electron-updater": "^6.6.2", "electron-updater": "^6.6.2",
"ws": "^8.14.2", "ws": "^8.14.2",
"zod": "^3.23.8" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "24.6.0", "@types/node": "24.6.0",
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -31,8 +31,9 @@
"open:android": "open -a \"Android Studio\" examples/basic-usage/android" "open:android": "open -a \"Android Studio\" examples/basic-usage/android"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.14",
"@types/react": "^18.0.25", "@types/react": "^18.0.25",
"expo-module-scripts": "^3.5.2", "expo-module-scripts": "^5.0.8",
"expo-modules-core": "^2.0.4" "expo-modules-core": "^2.0.4"
}, },
"peerDependencies": { "peerDependencies": {
+6 -1
View File
@@ -4,7 +4,12 @@
"compilerOptions": { "compilerOptions": {
"outDir": "./build", "outDir": "./build",
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler" "moduleResolution": "bundler",
"typeRoots": [
"./node_modules/expo-module-scripts/ts-declarations",
"./node_modules/@types",
"../../node_modules/@types"
]
}, },
"include": ["./src"], "include": ["./src"],
"exclude": ["**/__mocks__/*", "**/__tests__/*"] "exclude": ["**/__mocks__/*", "**/__tests__/*"]
+1 -1
View File
@@ -26,7 +26,7 @@
"test": "vitest run" "test": "vitest run"
}, },
"dependencies": { "dependencies": {
"zod": "^3.23.8" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.9.0", "@types/node": "^20.9.0",
+82 -85
View File
@@ -199,7 +199,7 @@ const AgentSelectOptionSchema = z.object({
label: z.string(), label: z.string(),
description: z.string().optional(), description: z.string().optional(),
isDefault: z.boolean().optional(), isDefault: z.boolean().optional(),
metadata: z.record(z.unknown()).optional(), metadata: z.record(z.string(), z.unknown()).optional(),
}); });
export const AgentFeatureToggleSchema = z.object({ export const AgentFeatureToggleSchema = z.object({
@@ -235,7 +235,7 @@ const AgentModelDefinitionSchema: z.ZodType<AgentModelDefinition> = z
label: z.string(), label: z.string(),
description: z.string().optional(), description: z.string().optional(),
isDefault: z.boolean().optional(), isDefault: z.boolean().optional(),
metadata: z.record(z.unknown()).optional(), metadata: z.record(z.string(), z.unknown()).optional(),
thinkingOptions: z.array(AgentSelectOptionSchema).optional(), thinkingOptions: z.array(AgentSelectOptionSchema).optional(),
defaultThinkingOptionId: z.string().optional(), defaultThinkingOptionId: z.string().optional(),
}) })
@@ -285,21 +285,21 @@ const McpStdioServerConfigSchema = z.object({
type: z.literal("stdio"), type: z.literal("stdio"),
command: z.string(), command: z.string(),
args: z.array(z.string()).optional(), args: z.array(z.string()).optional(),
env: z.record(z.string()).optional(), env: z.record(z.string(), z.string()).optional(),
alwaysLoad: z.boolean().optional(), alwaysLoad: z.boolean().optional(),
}); });
const McpHttpServerConfigSchema = z.object({ const McpHttpServerConfigSchema = z.object({
type: z.literal("http"), type: z.literal("http"),
url: z.string(), url: z.string(),
headers: z.record(z.string()).optional(), headers: z.record(z.string(), z.string()).optional(),
alwaysLoad: z.boolean().optional(), alwaysLoad: z.boolean().optional(),
}); });
const McpSseServerConfigSchema = z.object({ const McpSseServerConfigSchema = z.object({
type: z.literal("sse"), type: z.literal("sse"),
url: z.string(), url: z.string(),
headers: z.record(z.string()).optional(), headers: z.record(z.string(), z.string()).optional(),
alwaysLoad: z.boolean().optional(), alwaysLoad: z.boolean().optional(),
}); });
@@ -315,7 +315,7 @@ const AgentSessionConfigSchema = z.object({
modeId: z.string().optional(), modeId: z.string().optional(),
model: z.string().optional(), model: z.string().optional(),
thinkingOptionId: z.string().optional(), thinkingOptionId: z.string().optional(),
featureValues: z.record(z.unknown()).optional(), featureValues: z.record(z.string(), z.unknown()).optional(),
title: z.string().trim().min(1).max(MAX_EXPLICIT_AGENT_TITLE_CHARS).optional().nullable(), title: z.string().trim().min(1).max(MAX_EXPLICIT_AGENT_TITLE_CHARS).optional().nullable(),
approvalPolicy: z.string().optional(), approvalPolicy: z.string().optional(),
sandboxMode: z.string().optional(), sandboxMode: z.string().optional(),
@@ -323,16 +323,16 @@ const AgentSessionConfigSchema = z.object({
webSearch: z.boolean().optional(), webSearch: z.boolean().optional(),
extra: z extra: z
.object({ .object({
codex: z.record(z.unknown()).optional(), codex: z.record(z.string(), z.unknown()).optional(),
claude: z.record(z.unknown()).optional(), claude: z.record(z.string(), z.unknown()).optional(),
}) })
.partial() .partial()
.optional(), .optional(),
systemPrompt: z.string().optional(), systemPrompt: z.string().optional(),
mcpServers: z.record(McpServerConfigSchema).optional(), mcpServers: z.record(z.string(), McpServerConfigSchema).optional(),
}); });
const AgentPermissionUpdateSchema = z.record(z.unknown()); const AgentPermissionUpdateSchema = z.record(z.string(), z.unknown());
const AgentPermissionActionSchema = z.object({ const AgentPermissionActionSchema = z.object({
id: z.string(), id: z.string(),
label: z.string(), label: z.string(),
@@ -345,7 +345,7 @@ export const AgentPermissionResponseSchema: z.ZodType<AgentPermissionResponse> =
z.object({ z.object({
behavior: z.literal("allow"), behavior: z.literal("allow"),
selectedActionId: z.string().optional(), selectedActionId: z.string().optional(),
updatedInput: z.record(z.unknown()).optional(), updatedInput: z.record(z.string(), z.unknown()).optional(),
updatedPermissions: z.array(AgentPermissionUpdateSchema).optional(), updatedPermissions: z.array(AgentPermissionUpdateSchema).optional(),
}), }),
z.object({ z.object({
@@ -356,23 +356,20 @@ export const AgentPermissionResponseSchema: z.ZodType<AgentPermissionResponse> =
}), }),
]); ]);
export const AgentPermissionRequestPayloadSchema: z.ZodType< export const AgentPermissionRequestPayloadSchema: z.ZodType<AgentPermissionRequest, unknown> =
AgentPermissionRequest, z.object({
z.ZodTypeDef, id: z.string(),
unknown provider: AgentProviderSchema,
> = z.object({ name: z.string(),
id: z.string(), kind: z.enum(["tool", "plan", "question", "mode", "other"]),
provider: AgentProviderSchema, title: z.string().optional(),
name: z.string(), description: z.string().optional(),
kind: z.enum(["tool", "plan", "question", "mode", "other"]), input: z.record(z.string(), z.unknown()).optional(),
title: z.string().optional(), detail: z.lazy(() => ToolCallDetailPayloadSchema).optional(),
description: z.string().optional(), suggestions: z.array(AgentPermissionUpdateSchema).optional(),
input: z.record(z.unknown()).optional(), actions: z.array(AgentPermissionActionSchema).optional(),
detail: z.lazy(() => ToolCallDetailPayloadSchema).optional(), metadata: z.record(z.string(), z.unknown()).optional(),
suggestions: z.array(AgentPermissionUpdateSchema).optional(), });
actions: z.array(AgentPermissionActionSchema).optional(),
metadata: z.record(z.unknown()).optional(),
});
const UnknownValueSchema = z.union([ const UnknownValueSchema = z.union([
z.null(), z.null(),
@@ -410,8 +407,9 @@ const WorktreeSetupDetailPayloadSchema = z.object({
truncated: z.boolean().optional(), truncated: z.boolean().optional(),
}); });
const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail, z.ZodTypeDef, unknown> = const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail, unknown> = z.discriminatedUnion(
z.discriminatedUnion("type", [ "type",
[
WorktreeSetupDetailPayloadSchema, WorktreeSetupDetailPayloadSchema,
z.object({ z.object({
type: z.literal("shell"), type: z.literal("shell"),
@@ -505,7 +503,8 @@ const ToolCallDetailPayloadSchema: z.ZodType<ToolCallDetail, z.ZodTypeDef, unkno
input: UnknownValueSchema, input: UnknownValueSchema,
output: UnknownValueSchema, output: UnknownValueSchema,
}), }),
]); ],
);
const ToolCallBasePayloadSchema = z.object({ const ToolCallBasePayloadSchema = z.object({
type: z.literal("tool_call"), type: z.literal("tool_call"),
@@ -535,51 +534,49 @@ const ToolCallCanceledPayloadSchema = ToolCallBasePayloadSchema.extend({
error: z.null(), error: z.null(),
}); });
const ToolCallTimelineItemPayloadSchema: z.ZodType<ToolCallTimelineItem, z.ZodTypeDef, unknown> = const ToolCallTimelineItemPayloadSchema: z.ZodType<ToolCallTimelineItem, unknown> = z.union([
z.union([ ToolCallRunningPayloadSchema,
ToolCallRunningPayloadSchema, ToolCallCompletedPayloadSchema,
ToolCallCompletedPayloadSchema, ToolCallFailedPayloadSchema,
ToolCallFailedPayloadSchema, ToolCallCanceledPayloadSchema,
ToolCallCanceledPayloadSchema, ]);
]);
export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem, z.ZodTypeDef, unknown> = export const AgentTimelineItemPayloadSchema: z.ZodType<AgentTimelineItem, unknown> = z.union([
z.union([ z.object({
z.object({ type: z.literal("user_message"),
type: z.literal("user_message"), text: z.string(),
text: z.string(), messageId: z.string().optional(),
messageId: z.string().optional(), }),
}), z.object({
z.object({ type: z.literal("assistant_message"),
type: z.literal("assistant_message"), text: z.string(),
text: z.string(), messageId: z.string().optional(),
messageId: z.string().optional(), }),
}), z.object({
z.object({ type: z.literal("reasoning"),
type: z.literal("reasoning"), text: z.string(),
text: z.string(), }),
}), ToolCallTimelineItemPayloadSchema,
ToolCallTimelineItemPayloadSchema, z.object({
z.object({ type: z.literal("todo"),
type: z.literal("todo"), items: z.array(
items: z.array( z.object({
z.object({ text: z.string(),
text: z.string(), completed: z.boolean(),
completed: z.boolean(), }),
}), ),
), }),
}), z.object({
z.object({ type: z.literal("error"),
type: z.literal("error"), message: z.string(),
message: z.string(), }),
}), z.object({
z.object({ type: z.literal("compaction"),
type: z.literal("compaction"), status: z.enum(["loading", "completed"]),
status: z.enum(["loading", "completed"]), trigger: z.enum(["auto", "manual"]).optional(),
trigger: z.enum(["auto", "manual"]).optional(), preTokens: z.number().optional(),
preTokens: z.number().optional(), }),
}), ]);
]);
export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [ export const AgentStreamEventPayloadSchema = z.discriminatedUnion("type", [
z.object({ z.object({
@@ -755,7 +752,7 @@ export const AudioPlayedMessageSchema = z.object({
}); });
const AgentDirectoryFilterSchema = z.object({ const AgentDirectoryFilterSchema = z.object({
labels: z.record(z.string()).optional(), labels: z.record(z.string(), z.string()).optional(),
projectKeys: z.array(z.string()).optional(), projectKeys: z.array(z.string()).optional(),
statuses: z.array(AgentStatusSchema).optional(), statuses: z.array(AgentStatusSchema).optional(),
includeArchived: z.boolean().optional(), includeArchived: z.boolean().optional(),
@@ -786,7 +783,7 @@ export const UpdateAgentRequestMessageSchema = z.object({
type: z.literal("update_agent_request"), type: z.literal("update_agent_request"),
agentId: z.string(), agentId: z.string(),
name: z.string().optional(), name: z.string().optional(),
labels: z.record(z.string()).optional(), labels: z.record(z.string(), z.string()).optional(),
requestId: z.string(), requestId: z.string(),
}); });
@@ -1138,18 +1135,18 @@ export type CreateAgentWorktreeTarget = z.infer<typeof CreateAgentWorktreeTarget
export const CreateAgentRequestMessageSchema = z.object({ export const CreateAgentRequestMessageSchema = z.object({
type: z.literal("create_agent_request"), type: z.literal("create_agent_request"),
config: AgentSessionConfigSchema, config: AgentSessionConfigSchema,
env: z.record(z.string()).optional(), env: z.record(z.string(), z.string()).optional(),
workspaceId: z.string().optional(), workspaceId: z.string().optional(),
worktreeName: z.string().optional(), worktreeName: z.string().optional(),
initialPrompt: z.string().optional(), initialPrompt: z.string().optional(),
clientMessageId: z.string().optional(), clientMessageId: z.string().optional(),
outputSchema: z.record(z.unknown()).optional(), outputSchema: z.record(z.string(), z.unknown()).optional(),
images: z.array(ImageAttachmentSchema).optional(), images: z.array(ImageAttachmentSchema).optional(),
attachments: AgentAttachmentsSchema, attachments: AgentAttachmentsSchema,
git: GitSetupOptionsSchema.optional(), git: GitSetupOptionsSchema.optional(),
worktree: CreateAgentWorktreeTargetSchema.optional(), worktree: CreateAgentWorktreeTargetSchema.optional(),
autoArchive: z.boolean().optional(), autoArchive: z.boolean().optional(),
labels: z.record(z.string()).default({}), labels: z.record(z.string(), z.string()).default({}),
requestId: z.string(), requestId: z.string(),
}); });
@@ -1205,7 +1202,7 @@ export const ImportAgentRequestMessageSchema = z.object({
sessionId: z.string().optional(), sessionId: z.string().optional(),
providerHandleId: z.string().optional(), providerHandleId: z.string().optional(),
cwd: z.string().optional(), cwd: z.string().optional(),
labels: z.record(z.string()).optional(), labels: z.record(z.string(), z.string()).optional(),
requestId: z.string(), requestId: z.string(),
}); });
@@ -1832,7 +1829,7 @@ const ListCommandsDraftConfigSchema = z.object({
modeId: z.string().optional(), modeId: z.string().optional(),
model: z.string().optional(), model: z.string().optional(),
thinkingOptionId: z.string().optional(), thinkingOptionId: z.string().optional(),
featureValues: z.record(z.unknown()).optional(), featureValues: z.record(z.string(), z.unknown()).optional(),
}); });
export const ListProviderFeaturesRequestMessageSchema = z.object({ export const ListProviderFeaturesRequestMessageSchema = z.object({
@@ -2094,7 +2091,7 @@ export const ActivityLogPayloadSchema = z.object({
timestamp: z.coerce.date(), timestamp: z.coerce.date(),
type: z.enum(["transcript", "assistant", "tool_call", "tool_result", "error", "system"]), type: z.enum(["transcript", "assistant", "tool_call", "tool_result", "error", "system"]),
content: z.string(), content: z.string(),
metadata: z.record(z.unknown()).optional(), metadata: z.record(z.string(), z.unknown()).optional(),
}); });
export const ActivityLogMessageSchema = z.object({ export const ActivityLogMessageSchema = z.object({
@@ -2241,7 +2238,7 @@ export const ServerInfoStatusPayloadSchema = z
serverId: z.string().trim().min(1), serverId: z.string().trim().min(1),
hostname: ServerInfoHostnameSchema.optional(), hostname: ServerInfoHostnameSchema.optional(),
version: ServerInfoVersionSchema.optional(), version: ServerInfoVersionSchema.optional(),
capabilities: ServerCapabilitiesFromUnknownSchema, capabilities: ServerCapabilitiesFromUnknownSchema.optional(),
// COMPAT(providersSnapshot): added in v0.1.48, remove gating when all clients use snapshot // COMPAT(providersSnapshot): added in v0.1.48, remove gating when all clients use snapshot
features: z features: z
.object({ .object({
@@ -3486,7 +3483,7 @@ export const PullRequestTimelineResponseSchema = z.object({
githubFeaturesEnabled: z.boolean().optional().default(true), githubFeaturesEnabled: z.boolean().optional().default(true),
}) })
.optional() .optional()
.default({}), .prefault({}),
}); });
export const CheckoutSwitchBranchResponseSchema = z.object({ export const CheckoutSwitchBranchResponseSchema = z.object({
@@ -33,6 +33,34 @@ describe("paseo config schema", () => {
}); });
}); });
it("normalizes partial worktree lifecycle config without dropping present commands", () => {
expect(
PaseoConfigSchema.parse({
worktree: {
setup: 'echo "setup ran" > setup.log',
},
}),
).toEqual({
worktree: {
setup: ['echo "setup ran" > setup.log'],
teardown: [],
},
});
expect(
PaseoConfigSchema.parse({
worktree: {
teardown: ["npm run clean"],
},
}),
).toEqual({
worktree: {
setup: [],
teardown: ["npm run clean"],
},
});
});
it("parses all metadata generation instruction entries", () => { it("parses all metadata generation instruction entries", () => {
expect( expect(
PaseoConfigSchema.parse({ PaseoConfigSchema.parse({
+2 -2
View File
@@ -58,8 +58,8 @@ export const PaseoConfigRawSchema = z
.passthrough(); .passthrough();
export const WorktreeConfigSchema = PaseoWorktreeConfigRawSchema.extend({ export const WorktreeConfigSchema = PaseoWorktreeConfigRawSchema.extend({
setup: z.unknown().transform(normalizeLifecycleCommands), setup: z.unknown().optional().transform(normalizeLifecycleCommands),
teardown: z.unknown().transform(normalizeLifecycleCommands), teardown: z.unknown().optional().transform(normalizeLifecycleCommands),
}) })
.passthrough() .passthrough()
.catch({ setup: [], teardown: [] }); .catch({ setup: [], teardown: [] });
+5 -5
View File
@@ -24,7 +24,7 @@ export const ProviderCommandSchema = z.discriminatedUnion("mode", [
export const ProviderRuntimeSettingsSchema = z.object({ export const ProviderRuntimeSettingsSchema = z.object({
command: ProviderCommandSchema.optional(), command: ProviderCommandSchema.optional(),
env: z.record(z.string()).optional(), env: z.record(z.string(), z.string()).optional(),
disallowedTools: z.array(z.string()).optional(), disallowedTools: z.array(z.string()).optional(),
}); });
@@ -48,8 +48,8 @@ export const ProviderOverrideSchema = z.object({
label: z.string().optional(), label: z.string().optional(),
description: z.string().optional(), description: z.string().optional(),
command: z.array(z.string().min(1)).min(1).optional(), command: z.array(z.string().min(1)).min(1).optional(),
env: z.record(z.string()).optional(), env: z.record(z.string(), z.string()).optional(),
params: z.record(z.unknown()).optional(), params: z.record(z.string(), z.unknown()).optional(),
models: z.array(ProviderProfileModelSchema).optional(), models: z.array(ProviderProfileModelSchema).optional(),
additionalModels: z.array(ProviderProfileModelSchema).optional(), additionalModels: z.array(ProviderProfileModelSchema).optional(),
disallowedTools: z.array(z.string()).optional(), disallowedTools: z.array(z.string()).optional(),
@@ -61,7 +61,7 @@ const BUILTIN_PROVIDER_IDS = ["claude", "codex", "copilot", "opencode", "pi", "o
const PROVIDER_ID_PATTERN = /^[a-z][a-z0-9-]*$/; const PROVIDER_ID_PATTERN = /^[a-z][a-z0-9-]*$/;
export const ProviderOverridesSchema = z export const ProviderOverridesSchema = z
.record(ProviderOverrideSchema) .record(z.string(), ProviderOverrideSchema)
.superRefine((providers, ctx) => { .superRefine((providers, ctx) => {
const builtinProviderIdSet = new Set<string>(BUILTIN_PROVIDER_IDS); const builtinProviderIdSet = new Set<string>(BUILTIN_PROVIDER_IDS);
const validExtendsValues = new Set<string>([...BUILTIN_PROVIDER_IDS, "acp"]); const validExtendsValues = new Set<string>([...BUILTIN_PROVIDER_IDS, "acp"]);
@@ -111,7 +111,7 @@ export const ProviderOverridesSchema = z
}); });
export const AgentProviderRuntimeSettingsMapSchema = z export const AgentProviderRuntimeSettingsMapSchema = z
.record(ProviderRuntimeSettingsSchema) .record(z.string(), ProviderRuntimeSettingsSchema)
.superRefine((providers, ctx) => { .superRefine((providers, ctx) => {
for (const providerId of Object.keys(providers)) { for (const providerId of Object.keys(providers)) {
const parsedProviderId = AgentProviderSchema.safeParse(providerId); const parsedProviderId = AgentProviderSchema.safeParse(providerId);
@@ -10,11 +10,11 @@ import {
const ScheduleCreateTargetSchema = z.discriminatedUnion("type", [ const ScheduleCreateTargetSchema = z.discriminatedUnion("type", [
z.object({ z.object({
type: z.literal("self"), type: z.literal("self"),
agentId: z.string().uuid(), agentId: z.guid(),
}), }),
z.object({ z.object({
type: z.literal("agent"), type: z.literal("agent"),
agentId: z.string().uuid(), agentId: z.guid(),
}), }),
z.object({ z.object({
type: z.literal("new-agent"), type: z.literal("new-agent"),
+6 -6
View File
@@ -20,7 +20,7 @@ export type ScheduleCadence = z.infer<typeof ScheduleCadenceSchema>;
export const ScheduleTargetSchema = z.discriminatedUnion("type", [ export const ScheduleTargetSchema = z.discriminatedUnion("type", [
z.object({ z.object({
type: z.literal("agent"), type: z.literal("agent"),
agentId: z.string().uuid(), agentId: z.guid(),
}), }),
z.object({ z.object({
type: z.literal("new-agent"), type: z.literal("new-agent"),
@@ -35,16 +35,16 @@ export const ScheduleTargetSchema = z.discriminatedUnion("type", [
sandboxMode: z.string().trim().min(1).optional(), sandboxMode: z.string().trim().min(1).optional(),
networkAccess: z.boolean().optional(), networkAccess: z.boolean().optional(),
webSearch: z.boolean().optional(), webSearch: z.boolean().optional(),
featureValues: z.record(z.unknown()).optional(), featureValues: z.record(z.string(), z.unknown()).optional(),
extra: z extra: z
.object({ .object({
codex: z.record(z.unknown()).optional(), codex: z.record(z.string(), z.unknown()).optional(),
claude: z.record(z.unknown()).optional(), claude: z.record(z.string(), z.unknown()).optional(),
}) })
.partial() .partial()
.optional(), .optional(),
systemPrompt: z.string().optional(), systemPrompt: z.string().optional(),
mcpServers: z.record(z.unknown()).optional(), mcpServers: z.record(z.string(), z.unknown()).optional(),
}), }),
}), }),
]); ]);
@@ -56,7 +56,7 @@ export const ScheduleRunSchema = z.object({
startedAt: z.string(), startedAt: z.string(),
endedAt: z.string().nullable(), endedAt: z.string().nullable(),
status: z.enum(["running", "succeeded", "failed"]), status: z.enum(["running", "succeeded", "failed"]),
agentId: z.string().uuid().nullable(), agentId: z.guid().nullable(),
output: z.string().nullable(), output: z.string().nullable(),
error: z.string().nullable(), error: z.string().nullable(),
}); });
+4 -4
View File
@@ -63,7 +63,8 @@
}, },
"dependencies": { "dependencies": {
"@agentclientprotocol/sdk": "^0.17.1", "@agentclientprotocol/sdk": "^0.17.1",
"@anthropic-ai/claude-agent-sdk": "^0.2.133", "@anthropic-ai/claude-agent-sdk": "^0.3.181",
"@anthropic-ai/sdk": "^0.104.2",
"@getpaseo/client": "0.1.97-beta.3", "@getpaseo/client": "0.1.97-beta.3",
"@getpaseo/highlight": "0.1.97-beta.3", "@getpaseo/highlight": "0.1.97-beta.3",
"@getpaseo/protocol": "0.1.97-beta.3", "@getpaseo/protocol": "0.1.97-beta.3",
@@ -81,7 +82,7 @@
"lru-cache": "^11.5.1", "lru-cache": "^11.5.1",
"mnemonic-id": "^3.2.7", "mnemonic-id": "^3.2.7",
"node-pty": "1.2.0-beta.11", "node-pty": "1.2.0-beta.11",
"openai": "^4.20.0", "openai": "^6.44.0",
"p-limit": "^7.3.0", "p-limit": "^7.3.0",
"p-memoize": "^8.0.0", "p-memoize": "^8.0.0",
"pino": "^10.2.0", "pino": "^10.2.0",
@@ -94,8 +95,7 @@
"uuid": "^9.0.1", "uuid": "^9.0.1",
"which": "^5.0.0", "which": "^5.0.0",
"ws": "^8.14.2", "ws": "^8.14.2",
"zod": "^3.23.8", "zod": "^4.4.3"
"zod-to-json-schema": "^3.25.1"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.56.1", "@playwright/test": "^1.56.1",
@@ -2,7 +2,7 @@ import { fileURLToPath } from "node:url";
import path from "node:path"; import path from "node:path";
import fs from "node:fs"; import fs from "node:fs";
import { zodToJsonSchema } from "zod-to-json-schema"; import { z } from "zod";
import { PersistedConfigSchema } from "../src/server/persisted-config.js"; import { PersistedConfigSchema } from "../src/server/persisted-config.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@@ -13,9 +13,12 @@ function main() {
const outPath = path.join(repoRoot, "packages/website/public/schemas/paseo.config.v1.json"); const outPath = path.join(repoRoot, "packages/website/public/schemas/paseo.config.v1.json");
fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.mkdirSync(path.dirname(outPath), { recursive: true });
const schema = zodToJsonSchema(PersistedConfigSchema, { const schema = z.toJSONSchema(PersistedConfigSchema, {
name: "PaseoConfigV1", target: "draft-07",
unrepresentable: "any",
io: "input",
}); });
schema.title = "PaseoConfigV1";
fs.writeFileSync(outPath, JSON.stringify(schema, null, 2) + "\n", "utf8"); fs.writeFileSync(outPath, JSON.stringify(schema, null, 2) + "\n", "utf8");
process.stdout.write(`Wrote ${outPath}\n`); process.stdout.write(`Wrote ${outPath}\n`);
@@ -370,7 +370,7 @@ interface SubscriptionRecord {
} }
const BUSY_STATUSES: Set<AgentLifecycleStatus> = new Set(["initializing", "running"]); const BUSY_STATUSES: Set<AgentLifecycleStatus> = new Set(["initializing", "running"]);
const AgentIdSchema = z.string().uuid(); const AgentIdSchema = z.guid();
function isAgentBusy(status: AgentLifecycleStatus): boolean { function isAgentBusy(status: AgentLifecycleStatus): boolean {
return BUSY_STATUSES.has(status); return BUSY_STATUSES.has(status);
@@ -1,5 +1,4 @@
import { z } from "zod"; import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import Ajv, { type ErrorObject, type Options as AjvOptions } from "ajv"; import Ajv, { type ErrorObject, type Options as AjvOptions } from "ajv";
import type { AgentProvider, AgentSessionConfig } from "./agent-sdk-types.js"; import type { AgentProvider, AgentSessionConfig } from "./agent-sdk-types.js";
import type { AgentManager } from "./agent-manager.js"; import type { AgentManager } from "./agent-manager.js";
@@ -106,16 +105,19 @@ interface SchemaValidator<T> {
validate: (value: unknown) => { ok: true; value: T } | { ok: false; errors: string[] }; validate: (value: unknown) => { ok: true; value: T } | { ok: false; errors: string[] };
} }
function isZodSchema(value: unknown): value is z.ZodTypeAny { function isZodSchema(value: unknown): value is z.ZodType {
return typeof (value as z.ZodTypeAny | undefined)?.safeParse === "function"; return typeof (value as z.ZodType | undefined)?.safeParse === "function";
} }
function buildZodValidator<T>(schema: z.ZodTypeAny, schemaName: string): SchemaValidator<T> { function buildZodValidator<T>(schema: z.ZodType, schemaName: string): SchemaValidator<T> {
const zodToJsonSchemaAny = zodToJsonSchema as unknown as ( const jsonSchema = z.toJSONSchema(schema, {
input: z.ZodTypeAny, target: "draft-07",
name?: string, unrepresentable: "any",
) => JsonSchema; io: "input",
const jsonSchema = zodToJsonSchemaAny(schema, schemaName); }) as JsonSchema;
if (typeof jsonSchema.title !== "string") {
jsonSchema.title = schemaName;
}
return { return {
jsonSchema, jsonSchema,
validate: (value) => { validate: (value) => {
@@ -14,10 +14,10 @@ const SERIALIZABLE_CONFIG_SCHEMA = z
modeId: z.string().nullable().optional(), modeId: z.string().nullable().optional(),
model: z.string().nullable().optional(), model: z.string().nullable().optional(),
thinkingOptionId: z.string().nullable().optional(), thinkingOptionId: z.string().nullable().optional(),
featureValues: z.record(z.unknown()).nullable().optional(), featureValues: z.record(z.string(), z.unknown()).nullable().optional(),
extra: z.record(z.any()).nullable().optional(), extra: z.record(z.string(), z.any()).nullable().optional(),
systemPrompt: z.string().nullable().optional(), systemPrompt: z.string().nullable().optional(),
mcpServers: z.record(z.any()).nullable().optional(), mcpServers: z.record(z.string(), z.any()).nullable().optional(),
}) })
.nullable() .nullable()
.optional(); .optional();
@@ -27,7 +27,7 @@ const PERSISTENCE_HANDLE_SCHEMA = z
provider: z.string(), provider: z.string(),
sessionId: z.string(), sessionId: z.string(),
nativeHandle: z.any().optional(), nativeHandle: z.any().optional(),
metadata: z.record(z.any()).optional(), metadata: z.record(z.string(), z.any()).optional(),
}) })
.nullable() .nullable()
.optional(); .optional();
@@ -42,7 +42,7 @@ const STORED_AGENT_SCHEMA = z.object({
lastActivityAt: z.string().optional(), lastActivityAt: z.string().optional(),
lastUserMessageAt: z.string().nullable().optional(), lastUserMessageAt: z.string().nullable().optional(),
title: z.string().nullable().optional(), title: z.string().nullable().optional(),
labels: z.record(z.string()).default({}), labels: z.record(z.string(), z.string()).default({}),
lastStatus: AgentStatusSchema.default("closed"), lastStatus: AgentStatusSchema.default("closed"),
lastModeId: z.string().nullable().optional(), lastModeId: z.string().nullable().optional(),
config: SERIALIZABLE_CONFIG_SCHEMA, config: SERIALIZABLE_CONFIG_SCHEMA,
@@ -53,7 +53,7 @@ const STORED_AGENT_SCHEMA = z.object({
model: z.string().nullable().optional(), model: z.string().nullable().optional(),
thinkingOptionId: z.string().nullable().optional(), thinkingOptionId: z.string().nullable().optional(),
modeId: z.string().nullable().optional(), modeId: z.string().nullable().optional(),
extra: z.record(z.unknown()).optional(), extra: z.record(z.string(), z.unknown()).optional(),
}) })
.optional(), .optional(),
features: z.array(AgentFeatureSchema).optional(), features: z.array(AgentFeatureSchema).optional(),
@@ -9,6 +9,8 @@ import { z } from "zod";
import { AGENT_WAIT_TIMEOUT_MS } from "./mcp-shared.js"; import { AGENT_WAIT_TIMEOUT_MS } from "./mcp-shared.js";
import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { createTestPaseoDaemon, type TestPaseoDaemon } from "../test-utils/paseo-daemon.js";
import { createTestAgentClients } from "../test-utils/fake-agent-client.js";
import type { AgentClient, AgentProvider, AgentSessionConfig } from "./agent-sdk-types.js";
import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels"; import { PARENT_AGENT_ID_LABEL } from "@getpaseo/protocol/agent-labels";
interface StructuredContent { interface StructuredContent {
@@ -31,7 +33,7 @@ function str(val: unknown): string {
} }
function recordArr(val: unknown): StructuredContent[] { function recordArr(val: unknown): StructuredContent[] {
return z.array(z.record(z.unknown())).parse(val); return z.array(z.record(z.string(), z.unknown())).parse(val);
} }
function expectAgentFeatureValue(snapshot: StructuredContent, featureId: string, value: unknown) { function expectAgentFeatureValue(snapshot: StructuredContent, featureId: string, value: unknown) {
@@ -142,6 +144,56 @@ let agentScopedClient: McpClient;
let parentAgentId: string; let parentAgentId: string;
let parentAgentCwd: string; let parentAgentCwd: string;
let worktreeRepoCwd: string; let worktreeRepoCwd: string;
let launchConfigsByProvider: Record<AgentProvider, AgentSessionConfig[]>;
function createRecordingAgentClients(): Record<AgentProvider, AgentClient> {
const baseClients = createTestAgentClients();
launchConfigsByProvider = {};
const wrappedClients: Record<AgentProvider, AgentClient> = {};
for (const [provider, client] of Object.entries(baseClients)) {
const launchConfigs: AgentSessionConfig[] = [];
launchConfigsByProvider[provider] = launchConfigs;
const wrappedClient: AgentClient = {
provider: client.provider,
capabilities: client.capabilities,
createSession: async (config, launchContext, options) => {
launchConfigs.push(config);
return await client.createSession(config, launchContext, options);
},
resumeSession: async (handle, overrides, launchContext) =>
await client.resumeSession(handle, overrides, launchContext),
listModels: async (options) => await client.listModels(options),
isAvailable: async () => await client.isAvailable(),
};
if (client.listModes) {
wrappedClient.listModes = async (options) => await client.listModes!(options);
}
if (client.resolveCreateConfig) {
wrappedClient.resolveCreateConfig = (input) => client.resolveCreateConfig!(input);
}
if (client.isCreateConfigUnattended) {
wrappedClient.isCreateConfigUnattended = (input) => client.isCreateConfigUnattended!(input);
}
if (client.listCommands) {
wrappedClient.listCommands = async (config) => await client.listCommands!(config);
}
if (client.listFeatures) {
wrappedClient.listFeatures = async (config) => await client.listFeatures!(config);
}
if (client.listImportableSessions) {
wrappedClient.listImportableSessions = async (options) =>
await client.listImportableSessions!(options);
}
if (client.importSession) {
wrappedClient.importSession = async (input, context) =>
await client.importSession!(input, context);
}
wrappedClients[provider] = wrappedClient;
}
return wrappedClients;
}
async function makeCwd(prefix: string): Promise<string> { async function makeCwd(prefix: string): Promise<string> {
return await mkdtemp(path.join(tempRoot, `${prefix}-`)); return await mkdtemp(path.join(tempRoot, `${prefix}-`));
@@ -232,7 +284,7 @@ beforeAll(async () => {
parentAgentCwd = await makeCwd("parent-agent-cwd"); parentAgentCwd = await makeCwd("parent-agent-cwd");
worktreeRepoCwd = await makeCwd("worktree-repo"); worktreeRepoCwd = await makeCwd("worktree-repo");
daemonHandle = await createTestPaseoDaemon(); daemonHandle = await createTestPaseoDaemon({ agentClients: createRecordingAgentClients() });
topLevelClient = await createMcpClient(`http://127.0.0.1:${daemonHandle.port}/mcp/agents`); topLevelClient = await createMcpClient(`http://127.0.0.1:${daemonHandle.port}/mcp/agents`);
const parentPayload = await callToolStructured(topLevelClient, "create_agent", { const parentPayload = await callToolStructured(topLevelClient, "create_agent", {
@@ -302,10 +354,11 @@ describe("Suite A: Core Fixes", () => {
try { try {
const listenTarget = daemonHandle.daemon.getListenTarget(); const listenTarget = daemonHandle.daemon.getListenTarget();
expect(listenTarget?.type).toBe("tcp"); expect(listenTarget?.type).toBe("tcp");
const cwd = await makeCwd("manager-direct-agent-cwd");
const snapshot = await daemonHandle.daemon.agentManager.createAgent({ const snapshot = await daemonHandle.daemon.agentManager.createAgent({
provider: "claude", provider: "claude",
cwd: await makeCwd("manager-direct-agent-cwd"), cwd,
title: "Manager direct parity agent", title: "Manager direct parity agent",
modeId: "bypassPermissions", modeId: "bypassPermissions",
}); });
@@ -317,20 +370,19 @@ describe("Suite A: Core Fixes", () => {
agentId, agentId,
}); });
expect(snapshot.config.mcpServers).toMatchObject({ const launchConfig = launchConfigsByProvider.claude
?.toReversed()
.find((config) => config.cwd === cwd);
expect(launchConfig?.mcpServers).toMatchObject({
paseo: { paseo: {
type: "http", type: "http",
url: expectedUrl, url: expectedUrl,
}, },
}); });
expect(snapshot.config.mcpServers?.paseo).toBeUndefined();
const liveAgent = daemonHandle.daemon.agentManager.getAgent(agentId); const liveAgent = daemonHandle.daemon.agentManager.getAgent(agentId);
expect(liveAgent?.config.mcpServers).toMatchObject({ expect(liveAgent?.config.mcpServers?.paseo).toBeUndefined();
paseo: {
type: "http",
url: expectedUrl,
},
});
} finally { } finally {
await archiveAgentIfPresent(agentId); await archiveAgentIfPresent(agentId);
} }
@@ -355,7 +407,7 @@ describe("Suite A: Core Fixes", () => {
expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true }); expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true });
const status = await callToolStructured(topLevelClient, "get_agent_status", { agentId }); const status = await callToolStructured(topLevelClient, "get_agent_status", { agentId });
const snapshot = z.record(z.unknown()).parse(status.snapshot); const snapshot = z.record(z.string(), z.unknown()).parse(status.snapshot);
expectAgentFeatureValue(snapshot, "test_feature", true); expectAgentFeatureValue(snapshot, "test_feature", true);
} finally { } finally {
await archiveAgentIfPresent(agentId); await archiveAgentIfPresent(agentId);
@@ -373,7 +425,7 @@ describe("Suite A: Core Fixes", () => {
expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true }); expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true });
const status = await callToolStructured(topLevelClient, "get_agent_status", { agentId }); const status = await callToolStructured(topLevelClient, "get_agent_status", { agentId });
const snapshot = z.record(z.unknown()).parse(status.snapshot); const snapshot = z.record(z.string(), z.unknown()).parse(status.snapshot);
expectAgentFeatureValue(snapshot, "test_feature", true); expectAgentFeatureValue(snapshot, "test_feature", true);
} finally { } finally {
await archiveAgentIfPresent(agentId); await archiveAgentIfPresent(agentId);
@@ -393,7 +445,7 @@ describe("Suite A: Core Fixes", () => {
expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true }); expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true });
const status = await callToolStructured(topLevelClient, "get_agent_status", { agentId }); const status = await callToolStructured(topLevelClient, "get_agent_status", { agentId });
const snapshot = z.record(z.unknown()).parse(status.snapshot); const snapshot = z.record(z.string(), z.unknown()).parse(status.snapshot);
expectAgentFeatureValue(snapshot, "test_feature", true); expectAgentFeatureValue(snapshot, "test_feature", true);
} finally { } finally {
await archiveAgentIfPresent(agentId); await archiveAgentIfPresent(agentId);
@@ -6,7 +6,6 @@ import { join, resolve as resolvePath } from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import Ajv from "ajv"; import Ajv from "ajv";
import { z } from "zod"; import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import { createTestLogger } from "../../test-utils/test-logger.js"; import { createTestLogger } from "../../test-utils/test-logger.js";
import { createAgentMcpServer } from "./mcp-server.js"; import { createAgentMcpServer } from "./mcp-server.js";
@@ -114,7 +113,11 @@ async function invokeToolWithParsedInput(
function expectOutputSchemaAccepts(tool: RegisteredMcpTool, data: unknown): void { function expectOutputSchemaAccepts(tool: RegisteredMcpTool, data: unknown): void {
expect(tool.outputSchema).toBeDefined(); expect(tool.outputSchema).toBeDefined();
const jsonSchema = zodToJsonSchema(tool.outputSchema as z.ZodTypeAny); const jsonSchema = z.toJSONSchema(tool.outputSchema as z.ZodType, {
target: "draft-07",
unrepresentable: "any",
io: "input",
});
const ajv = new Ajv({ allErrors: true, strict: false }); const ajv = new Ajv({ allErrors: true, strict: false });
const validate = ajv.compile(jsonSchema); const validate = ajv.compile(jsonSchema);
expect(validate(data), JSON.stringify(validate.errors, null, 2)).toBe(true); expect(validate(data), JSON.stringify(validate.errors, null, 2)).toBe(true);
@@ -123,7 +126,7 @@ function expectOutputSchemaAccepts(tool: RegisteredMcpTool, data: unknown): void
function agentsOf(response: { function agentsOf(response: {
structuredContent: LooseStructuredContent; structuredContent: LooseStructuredContent;
}): Array<Record<string, unknown>> { }): Array<Record<string, unknown>> {
return z.array(z.record(z.unknown())).parse(response.structuredContent.agents); return z.array(z.record(z.string(), z.unknown())).parse(response.structuredContent.agents);
} }
type AgentManagerSpies = ReturnType<typeof buildAgentManagerSpies>; type AgentManagerSpies = ReturnType<typeof buildAgentManagerSpies>;
@@ -3363,7 +3366,7 @@ describe("agent snapshot MCP serialization", () => {
const tool = registeredTool(server, "list_agents"); const tool = registeredTool(server, "list_agents");
const response = await tool.handler({}); const response = await tool.handler({});
const structured = z const structured = z
.object({ agents: z.array(z.record(z.unknown())) }) .object({ agents: z.array(z.record(z.string(), z.unknown())) })
.parse(response.structuredContent); .parse(response.structuredContent);
expect(structured).toEqual({ expect(structured).toEqual({
@@ -3485,7 +3488,7 @@ describe("agent snapshot MCP serialization", () => {
}); });
const tool = registeredTool(server, "get_agent_status"); const tool = registeredTool(server, "get_agent_status");
const response = await tool.handler({ agentId: "full-detail-agent" }); const response = await tool.handler({ agentId: "full-detail-agent" });
const snapshot = z.record(z.unknown()).parse(response.structuredContent.snapshot); const snapshot = z.record(z.string(), z.unknown()).parse(response.structuredContent.snapshot);
const parsed = AgentSnapshotPayloadSchema.safeParse(snapshot); const parsed = AgentSnapshotPayloadSchema.safeParse(snapshot);
if (!parsed.success) { if (!parsed.success) {
+10 -5
View File
@@ -169,9 +169,11 @@ function formatStructuredContentForModel(structuredContent: unknown): string {
return summary.length > 0 ? `${summary.join("\n")}\n\n${json}` : json; return summary.length > 0 ? `${summary.join("\n")}\n\n${json}` : json;
} }
function isZodSchema(value: unknown): value is z.ZodTypeAny { function isZodSchema(value: unknown): value is z.ZodType {
return ( return (
typeof value === "object" && value !== null && "_def" in value && "safeParseAsync" in value typeof value === "object" &&
value !== null &&
typeof (value as { safeParseAsync?: unknown }).safeParseAsync === "function"
); );
} }
@@ -714,7 +716,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
modeId: z.string().optional().describe("Session mode to configure before the first run."), modeId: z.string().optional().describe("Session mode to configure before the first run."),
thinkingOptionId: z.string().optional().describe("Thinking option ID."), thinkingOptionId: z.string().optional().describe("Thinking option ID."),
features: z features: z
.record(z.unknown()) .record(z.string(), z.unknown())
.optional() .optional()
.describe("Provider-specific feature values, for example { fast_mode: true } for Codex."), .describe("Provider-specific feature values, for example { fast_mode: true } for Codex."),
}) })
@@ -729,7 +731,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
.optional() .optional()
.describe("Thinking option ID. Pass null to clear."), .describe("Thinking option ID. Pass null to clear."),
features: z features: z
.record(z.unknown()) .record(z.string(), z.unknown())
.optional() .optional()
.describe("Provider-specific feature values, for example { fast_mode: true } for Codex."), .describe("Provider-specific feature values, for example { fast_mode: true } for Codex."),
}) })
@@ -739,7 +741,10 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom
modeId: z.string().optional().describe("Draft session mode ID."), modeId: z.string().optional().describe("Draft session mode ID."),
model: z.string().optional().describe("Draft model ID."), model: z.string().optional().describe("Draft model ID."),
thinkingOptionId: z.string().optional().describe("Draft thinking option ID."), thinkingOptionId: z.string().optional().describe("Draft thinking option ID."),
features: z.record(z.unknown()).optional().describe("Draft provider feature values."), features: z
.record(z.string(), z.unknown())
.optional()
.describe("Draft provider feature values."),
}) })
.strict(); .strict();
const agentToAgentInputSchema = { const agentToAgentInputSchema = {
@@ -66,8 +66,8 @@ const cases: ParityCase[] = [
}, },
{ {
label: "non-existent path (canonicalize fallback)", label: "non-existent path (canonicalize fallback)",
// realpath will throw; canonicalize falls back to NFC of input. SDK does // realpath will throw; canonicalize falls back to the SDK's
// the same, so paths should still match. // platform-specific normalization of the input.
build: async () => join(workspaceRoot, "this-path-was-never-created-" + randomUUID()), build: async () => join(workspaceRoot, "this-path-was-never-created-" + randomUUID()),
}, },
]; ];
@@ -6,8 +6,8 @@ import { join } from "node:path";
// Verbatim port of the Claude Agent SDK's project-directory encoding so // Verbatim port of the Claude Agent SDK's project-directory encoding so
// paseo computes the same `~/.claude/projects/<dir>` path the SDK does. // paseo computes the same `~/.claude/projects/<dir>` path the SDK does.
// The SDK ships only as a precompiled bundle; grep the JS source at // The SDK ships only as a precompiled bundle; grep the JS source at
// node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs for `function f1`, // node_modules/@anthropic-ai/claude-agent-sdk/sdk.mjs for `function Ar`,
// `function i4`, `function tB`, `S0=200`. // `function So`, `async function wn`, `function Dy`, `Ni=200`.
const PROJECT_DIR_LENGTH_CAP = 200; const PROJECT_DIR_LENGTH_CAP = 200;
@@ -32,20 +32,24 @@ export function claudeProjectDirSync(cwd: string, options?: ClaudeProjectDirOpti
async function canonicalize(input: string): Promise<string> { async function canonicalize(input: string): Promise<string> {
try { try {
return (await realpath(input)).normalize("NFC"); return normalizeProjectPath(await realpath(input));
} catch { } catch {
return input.normalize("NFC"); return normalizeProjectPath(input);
} }
} }
function canonicalizeSync(input: string): string { function canonicalizeSync(input: string): string {
try { try {
return realpathSync.native(input).normalize("NFC"); return normalizeProjectPath(realpathSync.native(input));
} catch { } catch {
return input.normalize("NFC"); return normalizeProjectPath(input);
} }
} }
function normalizeProjectPath(input: string): string {
return process.platform === "darwin" ? input.normalize("NFC") : input;
}
function encode(input: string): string { function encode(input: string): string {
const replaced = input.replace(/[^a-zA-Z0-9]/g, "-"); const replaced = input.replace(/[^a-zA-Z0-9]/g, "-");
if (replaced.length <= PROJECT_DIR_LENGTH_CAP) { if (replaced.length <= PROJECT_DIR_LENGTH_CAP) {
@@ -11,6 +11,61 @@ import {
export const CommandValueSchema = z.union([z.string(), z.array(z.string())]); export const CommandValueSchema = z.union([z.string(), z.array(z.string())]);
type ToolDetailSchema = z.ZodType;
// Zod 4 loses the generic output type when these branch helpers constrain
// schemas through z.ZodType, so the paired shape/value interfaces preserve the
// transform callback types without changing runtime parsing.
interface ToolDetailNameShape<
Name extends string,
InputSchema extends ToolDetailSchema,
OutputSchema extends ToolDetailSchema,
> {
name: z.ZodLiteral<Name>;
input: z.ZodNullable<InputSchema>;
output: z.ZodNullable<OutputSchema>;
}
interface ToolDetailToolNameShape<
Name extends string,
InputSchema extends ToolDetailSchema,
OutputSchema extends ToolDetailSchema,
> {
toolName: z.ZodLiteral<Name>;
input: z.ZodNullable<InputSchema>;
output: z.ZodNullable<OutputSchema>;
}
interface ToolDetailNameWithCwdShape<
Name extends string,
InputSchema extends ToolDetailSchema,
OutputSchema extends ToolDetailSchema,
> extends ToolDetailNameShape<Name, InputSchema, OutputSchema> {
cwd: z.ZodNullable<z.ZodOptional<z.ZodString>>;
}
interface ToolDetailNameValue<
Name extends string,
InputSchema extends ToolDetailSchema,
OutputSchema extends ToolDetailSchema,
> {
name: Name;
input: z.output<InputSchema> | null;
output: z.output<OutputSchema> | null;
}
interface ToolDetailToolNameValue<
Name extends string,
InputSchema extends ToolDetailSchema,
OutputSchema extends ToolDetailSchema,
> {
toolName: Name;
input: z.output<InputSchema> | null;
output: z.output<OutputSchema> | null;
}
interface ToolDetailNameWithCwdValue<
Name extends string,
InputSchema extends ToolDetailSchema,
OutputSchema extends ToolDetailSchema,
> extends ToolDetailNameValue<Name, InputSchema, OutputSchema> {
cwd?: string | null;
}
export const ToolShellInputSchema = z export const ToolShellInputSchema = z
.union([ .union([
z z
@@ -424,11 +479,13 @@ interface ToolReadOutputValue {
content?: string; content?: string;
} }
export const ToolReadOutputSchema: z.ZodType<ToolReadOutputValue, z.ZodTypeDef, unknown> = export const ToolReadOutputSchema: z.ZodType<ToolReadOutputValue, unknown> =
ToolReadOutputContentSchema; ToolReadOutputContentSchema;
export const ToolReadOutputWithPathSchema: z.ZodType<ToolReadOutputValue, z.ZodTypeDef, unknown> = export const ToolReadOutputWithPathSchema: z.ZodType<ToolReadOutputValue, unknown> = z.union([
z.union([ToolReadOutputContentSchema, ToolReadOutputPathSchema]); ToolReadOutputContentSchema,
ToolReadOutputPathSchema,
]);
export const ToolWriteContentSchema = z export const ToolWriteContentSchema = z
.object({ .object({
@@ -918,8 +975,8 @@ export function toFetchToolDetail(
export function toolDetailBranchByName< export function toolDetailBranchByName<
Name extends string, Name extends string,
InputSchema extends z.ZodTypeAny, InputSchema extends ToolDetailSchema,
OutputSchema extends z.ZodTypeAny, OutputSchema extends ToolDetailSchema,
>( >(
name: Name, name: Name,
inputSchema: InputSchema, inputSchema: InputSchema,
@@ -929,20 +986,23 @@ export function toolDetailBranchByName<
output: z.infer<OutputSchema> | null, output: z.infer<OutputSchema> | null,
) => ToolCallDetail | undefined, ) => ToolCallDetail | undefined,
) { ) {
const schema = z.object({ const shape: ToolDetailNameShape<Name, InputSchema, OutputSchema> = {
name: z.literal(name), name: z.literal(name),
input: inputSchema.nullable(), input: z.nullable(inputSchema),
output: outputSchema.nullable(), output: z.nullable(outputSchema),
}); };
const schema = z.object(shape);
return schema.transform((value: z.infer<typeof schema>) => { return schema.transform((value: z.infer<typeof schema>) => {
return mapper(value.input, value.output); // Zod v4 drops generic unknown-valued shape fields from object output inference here.
const parsedValue = value as ToolDetailNameValue<Name, InputSchema, OutputSchema>;
return mapper(parsedValue.input, parsedValue.output);
}); });
} }
export function toolDetailBranchByToolName< export function toolDetailBranchByToolName<
Name extends string, Name extends string,
InputSchema extends z.ZodTypeAny, InputSchema extends ToolDetailSchema,
OutputSchema extends z.ZodTypeAny, OutputSchema extends ToolDetailSchema,
>( >(
toolName: Name, toolName: Name,
inputSchema: InputSchema, inputSchema: InputSchema,
@@ -952,20 +1012,22 @@ export function toolDetailBranchByToolName<
output: z.infer<OutputSchema> | null, output: z.infer<OutputSchema> | null,
) => ToolCallDetail | undefined, ) => ToolCallDetail | undefined,
) { ) {
const schema = z.object({ const shape: ToolDetailToolNameShape<Name, InputSchema, OutputSchema> = {
toolName: z.literal(toolName), toolName: z.literal(toolName),
input: inputSchema.nullable(), input: z.nullable(inputSchema),
output: outputSchema.nullable(), output: z.nullable(outputSchema),
}); };
const schema = z.object(shape);
return schema.transform((value: z.infer<typeof schema>) => { return schema.transform((value: z.infer<typeof schema>) => {
return mapper(value.input, value.output); const parsedValue = value as ToolDetailToolNameValue<Name, InputSchema, OutputSchema>;
return mapper(parsedValue.input, parsedValue.output);
}); });
} }
export function toolDetailBranchByNameWithCwd< export function toolDetailBranchByNameWithCwd<
Name extends string, Name extends string,
InputSchema extends z.ZodTypeAny, InputSchema extends ToolDetailSchema,
OutputSchema extends z.ZodTypeAny, OutputSchema extends ToolDetailSchema,
>( >(
name: Name, name: Name,
inputSchema: InputSchema, inputSchema: InputSchema,
@@ -976,13 +1038,15 @@ export function toolDetailBranchByNameWithCwd<
cwd: string | null, cwd: string | null,
) => ToolCallDetail | undefined, ) => ToolCallDetail | undefined,
) { ) {
const schema = z.object({ const shape: ToolDetailNameWithCwdShape<Name, InputSchema, OutputSchema> = {
name: z.literal(name), name: z.literal(name),
input: inputSchema.nullable(), input: z.nullable(inputSchema),
output: outputSchema.nullable(), output: z.nullable(outputSchema),
cwd: z.string().optional().nullable(), cwd: z.string().optional().nullable(),
}); };
const schema = z.object(shape);
return schema.transform((value: z.infer<typeof schema>) => { return schema.transform((value: z.infer<typeof schema>) => {
return mapper(value.input, value.output, value.cwd ?? null); const parsedValue = value as ToolDetailNameWithCwdValue<Name, InputSchema, OutputSchema>;
return mapper(parsedValue.input, parsedValue.output, parsedValue.cwd ?? null);
}); });
} }
@@ -249,7 +249,7 @@ export const PersistedConfigSchema = z
// localhost service proxying remains always enabled. // localhost service proxying remains always enabled.
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
listen: z.string().optional(), listen: z.string().optional(),
publicBaseUrl: z.string().url().optional(), publicBaseUrl: z.url().optional(),
}) })
.strict() .strict()
.optional(), .optional(),
+1 -1
View File
@@ -544,7 +544,7 @@ const MIN_STREAMING_SEGMENT_DURATION_MS = 1000;
const MIN_STREAMING_SEGMENT_BYTES = Math.round( const MIN_STREAMING_SEGMENT_BYTES = Math.round(
PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS, PCM_BYTES_PER_MS * MIN_STREAMING_SEGMENT_DURATION_MS,
); );
const AgentIdSchema = z.string().uuid(); const AgentIdSchema = z.guid();
interface VoiceModeBaseConfig { interface VoiceModeBaseConfig {
systemPrompt?: string; systemPrompt?: string;
} }
@@ -45,9 +45,13 @@ export interface LocalSpeechSttLanguageConfig {
const NumberLikeSchema = z.union([z.number(), z.string().trim().min(1)]); const NumberLikeSchema = z.union([z.number(), z.string().trim().min(1)]);
const LanguageSchema = z.string().trim().min(1).default(DEFAULT_STT_LANGUAGE); const LanguageSchema = z.string().trim().min(1).default(DEFAULT_STT_LANGUAGE);
const OptionalFiniteNumberSchema = NumberLikeSchema.pipe(z.coerce.number().finite()).optional(); const OptionalFiniteNumberSchema = NumberLikeSchema.pipe(
z.coerce.number<string | number>().finite(),
).optional();
const OptionalIntegerSchema = NumberLikeSchema.pipe(z.coerce.number().int()).optional(); const OptionalIntegerSchema = NumberLikeSchema.pipe(
z.coerce.number<string | number>().int(),
).optional();
const LocalSpeechResolutionSchema = z.object({ const LocalSpeechResolutionSchema = z.object({
includeProviderConfig: z.boolean(), includeProviderConfig: z.boolean(),
@@ -85,9 +85,7 @@ function resolveDefaultModelId(role: DefaultModelRole): SherpaOnnxModelId {
export const DEFAULT_LOCAL_STT_MODEL = resolveDefaultModelId("stt"); export const DEFAULT_LOCAL_STT_MODEL = resolveDefaultModelId("stt");
export const DEFAULT_LOCAL_TTS_MODEL = resolveDefaultModelId("tts"); export const DEFAULT_LOCAL_TTS_MODEL = resolveDefaultModelId("tts");
function createModelIdSchema<T extends string>( function createModelIdSchema<T extends string>(modelIds: readonly T[]): z.ZodType<T, string> {
modelIds: readonly T[],
): z.ZodType<T, z.ZodTypeDef, string> {
const validIds = new Set<string>(modelIds); const validIds = new Set<string>(modelIds);
return z return z
.string() .string()
@@ -21,7 +21,9 @@ const OpenAiTtsModelSchema = z.enum(["tts-1", "tts-1-hd"]);
const NumberLikeSchema = z.union([z.number(), z.string().trim().min(1)]); const NumberLikeSchema = z.union([z.number(), z.string().trim().min(1)]);
const OptionalFiniteNumberSchema = NumberLikeSchema.pipe(z.coerce.number().finite()).optional(); const OptionalFiniteNumberSchema = NumberLikeSchema.pipe(
z.coerce.number<string | number>().finite(),
).optional();
const OptionalTrimmedStringSchema = z const OptionalTrimmedStringSchema = z
.string() .string()
@@ -78,7 +78,7 @@ type RegistryRecord = PersistedProjectRecord | PersistedWorkspaceRecord;
class FileBackedRegistry<TRecord extends RegistryRecord> { class FileBackedRegistry<TRecord extends RegistryRecord> {
private readonly filePath: string; private readonly filePath: string;
private readonly logger: Logger; private readonly logger: Logger;
private readonly schema: z.ZodType<TRecord, z.ZodTypeDef, unknown>; private readonly schema: z.ZodType<TRecord, unknown>;
private readonly getId: (record: TRecord) => string; private readonly getId: (record: TRecord) => string;
private loaded = false; private loaded = false;
private readonly cache = new Map<string, TRecord>(); private readonly cache = new Map<string, TRecord>();
@@ -87,7 +87,7 @@ class FileBackedRegistry<TRecord extends RegistryRecord> {
constructor(options: { constructor(options: {
filePath: string; filePath: string;
logger: Logger; logger: Logger;
schema: z.ZodType<TRecord, z.ZodTypeDef, unknown>; schema: z.ZodType<TRecord, unknown>;
getId: (record: TRecord) => string; getId: (record: TRecord) => string;
component: string; component: string;
}) { }) {
+2 -2
View File
@@ -13,8 +13,8 @@
"dependencies": { "dependencies": {
"@cloudflare/vite-plugin": "^1.29.1", "@cloudflare/vite-plugin": "^1.29.1",
"@cloudflare/workers-types": "^4.20260317.1", "@cloudflare/workers-types": "^4.20260317.1",
"@tanstack/react-router": "^1.166.4", "@tanstack/react-router": "^1.170.16",
"@tanstack/react-start": "^1.166.4", "@tanstack/react-start": "^1.168.26",
"framer-motion": "^12.35.2", "framer-motion": "^12.35.2",
"github-slugger": "^2.0.0", "github-slugger": "^2.0.0",
"lucide-react": "^1.7.0", "lucide-react": "^1.7.0",